[HN Gopher] Under Deconstruction: The State of Shopify's Monolith ___________________________________________________________________ Under Deconstruction: The State of Shopify's Monolith Author : kenrose Score : 205 points Date : 2020-09-17 15:03 UTC (7 hours ago) (HTM) web link (engineering.shopify.com) (TXT) w3m dump (engineering.shopify.com) | throwaway691999 wrote: | I think it's kind of bad that we have this trend to use "walls" | to enforce modularity. This whole thing about using "walls" to | enforce "developer behavior" is, in my humble opinion, the wrong | direction. | | If you think about it, almost all lack of modularity comes from | shared mutable variables. Segregate mutability away from the core | logic of your system and the smallest function in your | architecture will become as modular as a microservice. | | Really, any function that is stateless can be moved anywhere at | anytime and used anywhere without fear of it being creating a | permanent foothold in the architectural complexity of the system. | So if the code is getting to structured where you become afraid | of moving things... do this rather than build classes and walls | around all your subroutines. | | Remember as long as that add function doesn't mutate shared state | you know it has zero impact on any part of the system other than | it's output... you can replace it or copy it or use it | anywhere.... this is really all you need to do to improve | modularity of your system. | | >Again and again we pondered: How should components call each | other? | | I think this is what's tripping most people up. They think DI IOC | and OOP patterns are how you improve modularity. It's not. | Immutable functions are what improves modularity of your program. | The more immutable functions you have and the smaller they are | the more modular your program will be. Segregate IO and mutations | into tiny auxiliary functions away from your core logic which is | composed of pure immutable functions. | | >Circular dependencies are situations where for example component | A depends on component B but component B also depends on | component A. | | I've never seen circular dependencies happen with pure functions. | It's rare in practice. I think it occurs with objects because | when you want one method of an object you have to instantiate | that object which has a bunch of other methods and in turn | dependencies that could be circular to the current object you're | trying to call it from. In essence this kind of thing tends to | happen because when you call a method you're actually calling a | group of methods and state within a class and upon all those | dependencies as well increasing the chances of a circular | dependency. | | Still I've seen this issue occur with namespacing when you import | files. Walls aren't going to segregate this from happening. You | need to structure your dependencies as a tree. | kawsper wrote: | Does anyone know if the Storefront rendering described here[0] is | running Rails or something else? | | [0] https://engineering.shopify.com/blogs/engineering/how- | shopif... | rafaelfranca wrote: | The application is a Rack application reusing some of the | components of Rails, but it is not a conventional Rails | application given it doesn't need most of the framework. | sandGorgon wrote: | This is a brilliant brilliant article. | | Does anyone know how Shopify created it's Architecture Guild and | grew it ? The author talks about "should have done it earlier" | exterm wrote: | As the author, I would know :) | | Thank you for the praise. | | Ours kind of organically grew over time, but as I've been | keeping it alive for the last few years I have a pretty good | idea of how I would start it fresh. | | You probably have some people in the company who either know | much more about architecture than others, or are working on | projects that are more interesting in terms of architecture. | Find one of them, convince them to give a 15 min talk. | | Announce the talk widely within the company, tell people to | come to the new "architecture guild" slack channel you created | to get the details / invites. | | Schedule an hour to give plenty of time for discussions after | the talk. | | Repeat biweekly. | sandGorgon wrote: | Thanks for replying. | | How would you do it in a remote-first world? A zoom talk ? | | How does this go beyond that one talk - would you incorporate | aspects of this into official rewards/recognition ? | | Or is gratification good enough. Getting a zoom audience is | gonna be hard. | mochii wrote: | Very interesting read! Thank you for sharing. | meesterdude wrote: | Interesting read. I've seen a component based rails architecture | work wonders for cleaning up a codebase and allowing for the | benefits of a SOA encapsulation while still keeping everything | under a monolithic architecture (and avoiding the networking | nasties). Not such a fan of sorbet though, but hopefully | something better comes along. | gregkerzhner wrote: | Interesting article. We use a similar approach for our mobile | apps to allow multiple teams to develop their own modules | independently. | | Can anyone speak to what the advantages and disadvantages to such | an approach are as opposed to going full Kubernetes / | Microservices? Is it that deploys are riskier and you can't scale | separate pieces independently? | treis wrote: | Have y'all seen any issues around autoloading of classes/modules | in development? I've been working on a rails app composed of a | handful of engines and I've noticed that every so often classes | aren't loaded. 6 seems to be a lot better about it than 5 was. | mhoad wrote: | Rails 6 has a totally new code loader that was built | specifically to address those issues called Zeitwerk. Some | details here if you're interested | https://blog.bigbinary.com/2019/10/08/rails-6-introduces-new... | straws wrote: | A number of years ago, I worked on a team (~20 engineers in | total) that successfully carved off two relatively independent | portions of a large Rails app using engines. I'm happy to see | that Shopify is also using that strategy. | | I'm curious to know more what sorts of challenges they have | around managing dependencies across engines -- I think what we | were doing was fairly vanilla Rails, and we didn't have the | opportunity to run into those sorts of issues. | kogus wrote: | This is tangental, but I want applaud the mindset that produces | the phrase "the opportunity to run into those sorts of issues". | straws wrote: | I meant it more as a testament to how far you can get with a | Rails app before needing to consider using the power tools | :^) | JohnBooty wrote: | I worked on a large Rails monolith a few years back with a | similarly-sized team and we took the "components+engines" | approach too.... and it was a bit of a nightmare, honestly. It | sort of felt like the worst of both worlds, relative to | monoliths or microservices. | | I strongly suspect, but cannot prove, that we would have been | better off simply transitioning to "macroservices" -- breaking | the monolith up into several (as opposed to dozens) of | reasonably sized pieces. | | * We were encouraged to componitize everything. When I left, we | were up to a few dozen components, and the number was climbing | rapidly. I'm not sure if the approach itself was the problem, | or if the _flux_ during the transition period was the real pain | point. | | * We had no real enforcement of interfaces between components. | It was so easy to break things in other peoples' components. | | * Theoretically that breakage would be caught by tests. But to | catch that breakage, you needed to run the complete test suite | (30-60 minutes) rather than simply testing your own component | | * Essentially, it felt like we were suffering all the | disadvantages of microservices, with the exception of | coordinating deployments; from a devops perspective it was | still just a single monolithic deployment | | * We still had many of the problems associated with monoliths, | such as slow deployments, long test suite times, and extremely | high per-instance RAM usage | | * Various small tooling and debugging issues related to using | Rails but going too far "off the Rails" | | I'm looking forward to digging into the linked article and | learning how Shopify solved those issues. They seem to have | quite a bit of engineering firepower at their disposal. Our | management did not allow us to dedicate a lot of resources to | internal engineering concerns like this. | | (We essentially had one guy figuring it all out himself, and | due to internal politics he was forbidden from considering a | microservices or "macro services / multiple monoliths" | approach. He was talented and did the best he could, | considering) | exterm wrote: | > When I left, we were up to a few dozen components, and the | number was climbing rapidly. | | I should have included this in the blog post: The number of | components _needs_ to be kept small. Shopify's main monolith | is 2.8 million lines of code in 37 components, and I'd | actually like to get that number _down_. | | I like to compare this to the main navigation that we present | to our merchants. It's useful if it has 8 entries. It's not | useful if it has 400. | | In a way, components are the main navigation to our code | base. A developer should be able to look at what's in our | "components" folder and get a general impression of what the | system's capabilities are. | exterm wrote: | The answer to that question could probably fill another blog | post :D | | Long story short, Rails and dependency inversion equals lots of | friction. The whole framework is built on the assumption that | it's OK to access everything from everywhere, and over the | years we've built lots of tooling on top of those assumptions. | | E.g. we heavily use https://github.com/Shopify/identity_cache | with active record associations that cross component | boundaries. | | We also have a GraphQL implementation that is pretty closely | coupled to the active record layer and _really_ wants to reach | into all the components directly. | | All of those problems can be overcome, but this is definitely | an area where we have to working against "established" Rails | culture, and our own assumptions from the past. | sandGorgon wrote: | What's the difference between "componentization+engines" and | microservices? | | From a deployment perspective are your engines deployed and | scaled independently? | exterm wrote: | components are | | - same database - same runtime - same deployment - same | repository | | That said, I don't think this is an either/or. It's a | spectrum. you can have components within the same runtime | and repository that have separate databases, or components | that are using the same database but live in separate | repos, etc. | | From one monolithic app towards fully separated | microservices is a spectrum, and I think developers should | be enabled to move freely around that spectrum. | sandGorgon wrote: | I think components are the better option. Because it | allows for separation of concerns without introducing | deployment ...or worse : political complexity. | | I call them Micro-SDKs. | straws wrote: | I hope to hear more in the future! | | Do you envision any extension points to the way engines are | implemented that could better enforce boundaries? In our | engines, there was nothing that referenced another engine's | resources, leaving the main application to handle route | mapping and ActiveRecord associations between app models and | engine's models. | | I feel like the use-case for engines has long been around | supporting framework like functionality (Devise, Spree, etc), | but I wonder if there are changes to be made that better | support modularization for large apps. | exterm wrote: | > extension points to the way engines are implemented that | could better enforce boundaries | | Can you expand on that? I'm not sure I follow. | lmarcos wrote: | Great article. Main takeaway: microservices is not the only | option when managing big codebases. In a parallel universe I | imagine that the coolest trend in software development right now | is a tool for monoliths: all code in a single repo, independent | deployable components, contracts in the boundaries and mockable | dependant components where needed. As opposed to our universe in | which building microservices is the non-official way to go. | etaioinshrdlu wrote: | This is basically what I do and it's great :) | JamesSwift wrote: | And I will call that parallel universe: monorepo-verse | gen220 wrote: | What you describe is microservices developed in a monorepo, and | a lot of companies (including the one I currently work at) have | gone this route. | | Some people might disagree, but imo the cult of microservices | does not require 1 repo per microservice. | | The tools you describe are build-graph management tools (bazel | pants buck etc) and rpc tools (gRPC + protobufs, cap'n proto) | and they are indeed pretty cool, albeit to a niche crowd. | djohnston wrote: | I agree 100%. It gives you the boundaries but also the whole | world maps to a single revision in VCS | jurre wrote: | I think the key difference here is that there is no network | in between components in a componentized monolith, each | component runs the entire "monorepo" | dodobirdlord wrote: | Whether there's actually network between components is | something a platform team can handle based on their best | judgement. Having collections of containers that always run | together is a common pattern. | thebean11 wrote: | Is there really that big of an advantage to avoiding the | network boundary though? | nthj wrote: | Absolutely: | | * Avoid network and JSON serialization overhead | | * Perform larger refactorings or renamings without | considering deployment staggering or API versioning | | * testing locally is far easier | | * Debugging in production is far easier | | * Useful error stack traces are included for free | | * Avoid (probable in my experience, at least in larger | security software organizations) dependency on SecOps to | make network changes to support a refactoring or | introducing new components | | If an organization is or will pursue a FedRAMP | certification, as I understand it, that organization must | propose and receive approval every time data may hit a | network. Avoiding the network in that case may be the | difference between a 50-line MR that's merged before | lunch and a multi-week process involving multiple people. | closeparen wrote: | How are you getting around API versioning with | independently deployable components? | heavenlyblue wrote: | What is a network? | gen220 wrote: | FWIW, I think that gRPC/protobufs have pretty compelling | answers to each of the historically-valid complaints | you've listed here. | | - cpu cycle overhead: this is valid if the overhead is | very high or very important. otherwise, most companies | would love to trade off cpu cycles for dev productivity. | | - refactorings/renamings without deployment staggering. | protobufs were specifically designed with this in mind, | insofar as they support deprecating fields and whatnot. | However, writing a deprecatable-API is a skill, even with | protos. If you have many clients and want to redo | everything by scratch, you will have problems. | | - "testing locally" (which I take to mean integration | testing locally) is the only one that requires some | imagination to solve, assuming all your traffic is | guarded by short-term-lease certs issued by vault or | something similar. But even this is quite achievable. | | - error stack traces included for free: may I introduce | you to context.abort(). It's not a stack trace by | default, but you can actually wrap the stack trace into | the message if you so-care to. opentracing isn't quite | free, in a performance sense, but in a required-eng-time- | to-setup-and-maintain-sense, it is pretty cheap. | | - dependency on secops to make network changes: I've | never encountered this, but I bet you that a good | platform team can provide a system where application | teams effectively don't need to worry about this. It's | impossible to overcome this challenge in an existing | company that's used to doing things this way, though. | [deleted] | jurre wrote: | It depends, but it means you don't have to | serialize/deserialize data, deal with slow connections, | retries, network failures, circuit breakers etc | twunde wrote: | It depends on your speed requirements and whether calls | are being sent async or not. Also keep in mind that even | with internal apis, an api call is usually multiple | network boundaries (service1 --> service2 (potential DNS | lookup) --> WAF/security proxy --> Firewall --> Load | balancer --> SSL handshake --> server/container firewall | --> server/container). Then you get into whether the | service you're calling calls other apis etc. You can | quickly burn 50ms or more with multiple hops. If you're | trying to return responses within 200ms you now have very | little margin. | gen220 wrote: | Acknowledging that there are indeed many hops, I think it | _might_ be a bit disingenuous to say 50ms is easy to | burn, depending on what p-value we 're talking about. | | IIRC, a round trip service call at my current place of | work carries a p99 minimum latency (i.e. returning a | constant) of around 2ms. | | A _cold_ roundtrip obviously takes longer (because DNS, | ssl, etc). | | It depends on how many 9's you want within 10ms, but | there are various simple tricks (transparent to the | application developer) that a platform team can apply to | get you there. | | As a sidenote on calling other APIs, my anecdata suggests | that _most_ companies microservice call graphs are _at | most_ 3-4 services deep, with the vast majority being 1-2 | services deep. | | This doesn't show the call graph, but it does demonstrate | how many companies end up building a handful of services | that gatekeep the core data models, and the rest simply | compose over those services: https://twitter.com/adrianco | /status/441883572618948608/photo... | gen220 wrote: | I think there used to be, before "off-the-shelf" RPC | frameworks, service discovery, and the like were mature. | There still are, for very small companies. | | In 2020, if you have an eng count of >50: you use gRPC, | some sort of service discovery solution (consul, envoy, | whatever), and you basically never have to think about | the costs of network hops. Opentracing is also pretty | mature these days, although in my experience it's never | been necessary when I can read the source of the services | my code depends on. | | Network boundaries are really useful for enforcing | interface boundaries, because we should trust N>50 | programmers to correctly-implement bounded contexts as | much as we trust PG&E to maintain the power grid. | | That being said, if you have a small, crack team, bounded | contexts will take you all the way there and you don't | _need_ network boundaries to enforce them. | hliyan wrote: | I often find myself saying "never do at runtime what could be | done at compile time". | exterm wrote: | you don't work with Ruby eh? :D | WJW wrote: | The lack of compile time in the ruby world really makes it | difficult to do a lot of work there. :P | | There's a nice Ruby trick btw where you put significant | precalculations in constants, since the value of a constant | gets computed during program startup it still allow you to | do work "up front" instead of during a web request. | mhoad wrote: | I never knew that but that IS a cool trick. | skipants wrote: | I just want to caveat this as it is not a Ruby construct, | it's part of Ruby web servers. Because they are long- | living Ruby processes they are only loading files once (I | suppose it's similar to compiling). This means it runs | all globally-scoped code, which class definitions and | constants (generally) are. That's actually what Ruby | bootloaders like Spring and Zeus are doing on your dev | machine to speed up the load time when you use Rails | commands. They cache all that globally run stuff in their | own process. It's also why they run into a bunch of | issues when you have logic in your constant definitions. | owyn wrote: | Yep, that's a good trick. At a previous PHP shop we had a | large amount of static XML configuration (well it was | generated, but not that often). Converting it all to PHP | arrays and including it was significantly faster than | parsing the XML on each request, and then PHP would cache | that result too. Re-running the XML->PHP tool just caused | it to re-include/cache these giant arrays of static | config. It worked great. I mean, arguments about whether | that was a good design or not aside... | | (edit to reply since I can't reply to a reply to a reply) | | Yep, it is very common in lisp/smalltalk environments to | dump the state of the world to disk and re-load it later. | This is one of those tricks that gets relearned every | generation. :) | | For bonus credit apply this analogy to docker images. :) | JohnBooty wrote: | I did this in PHP once as well. I had to code a coupon | lookup site where people entered coupon codes and they | were verified against a database. I forget how many | coupons there were... pretty sure it was less than | 100,000. | | Anyway, I coded it up in my local dev environment. | Unfortunately, it turned out that I'd been mislead and | the actual deployment environment didn't have a database | server available. | | In desperation and facing a deadline, I dumped all the | lookup values into an array in a PHP file. As you said, | it was really quite performant. The first request after | starting the server was a bit slow (but not too bad... | still < 10 seconds I think) and after that things were | golden. | | I felt a bit dirty, but things worked and we got paid. | im3w1l wrote: | I heard emacs did that but went one step further, they | dump the memory of the interpreter post-init* and just | load it into memory when starting. | | * Some early step in the init process. May things are | still interpreted at init. | im3w1l wrote: | I heard emacs did that but went one step further, they | dump the memory of the interpreter post-init* and just | load it into memory when starting. | | * Some early step in the init process. Many things are | still interpreted at init. | aantix wrote: | In Ruby, the class definition is code as well. | ChiefOBrien wrote: | Reminds me of OSGi. Great idea, poor adoption, mostly due to | the complexity of the problem domain. Microservices are a lot | worse in that regard, yet remain a whole lot more popular, | sadly. | jakobmartz3 wrote: | interesting thought! | didibus wrote: | Ignoring some of the deployments and dependencies related | aspects of microservices Vs monolith, one aspect that has me | convinced against my own ideals is that a micro-service has a | "strong" boundary in that it is actually difficult and | effortful for a developer to cross it. | | This in turn has a positive effect in maintaining proper | boundary and putting the right amount of thought about the | interfaces and responssability of each component. | exterm wrote: | It's all tradeoffs. You get a stronger boundary, but you also | get a distributed system. | | Also, the first try of drawing boundaries will always be | varying degrees of wrong. If you have very strong boundaries | at this stage, iterating on them, moving responsibilities | around, can be harder. | | Also, with the right tooling it's definitely possible to | harden monolith internal boundaries to a comparable level. | | I can see though how many smaller companies would not be in a | position to build that tooling. | | Anyway... there is no either / or here, as I've explained in | another comment. What if you have components within a | monolith, but each component has its own database, for | example? What if test suites are completely isolated, so that | tests for component A can not access code in component B? | | You can get pretty strong boundaries with a few comparably | simple tricks. | joelbluminator wrote: | 2.8 million lines , 100 billion business. Rails can scale. | Thaxll wrote: | Well apparently not since all the work they have to do to make | it scale, using Java from day 1 would have solved a lot of | issues. | | We'll see in a couple of years after breaking down all those | app if they stick to rail to do everything. | joelbluminator wrote: | Yes, you're right. If only they sprinkled some Beans over | everything all their scale problems would just disappear. | Thousands of happy developers would have all worked on one | big happy Spring application with no problems whatsoever. | whycombagator wrote: | > Well apparently not since all the work they have to do to | make it scale, using Java from day 1 would have solved a lot | of issues. | | It is a lot of work, but scaling anything is. What issues, | specifically, are you alluding to that Java would solve? | | > We'll see in a couple of years after breaking down all | those app if they stick to rail to do everything. | | I'd bet they currently don't use Rails/Ruby for everything. | It's pretty rare for large companies to use just 1 | language/framework for all things. | Thaxll wrote: | > It is a lot of work, but scaling anything is. What | issues, specifically, are you alluding to that Java would | solve? | | Ruby is slow, Java is fast. When you have to modify the | runtime of a language because its too slow: https://enginee | ring.shopify.com/blogs/engineering/optimizing... | | Reading all those blogs from Shopify show that they spend a | lot of time fighting a slow language. | | It reminds me of Facebook and their Hack stuff, it's pretty | much the same in what Shopify is getting into, they have | something slow and really big and not way to get out of it | so they just poor money to make it fast even if it means | only the syntax resemble the original language. | | Some compagnies faced the same problem, quick quick release | something to iterate fast ( Rail / Python) but then after | when it gets too big you're in real troubles and stuck with | it. Twitter, Youtube, Facebook all had that problem. | crispyporkbites wrote: | Name a successful web company that hasn't had that | problem | nurettin wrote: | Everyone knows that only the best enterprise programmers | apply to java positions, and when they somehow manage to | pry themselves through the screening, they will help you | bicker undecisively for hours when adding even the smallest | functionality because they love bringing _everything_ to | the table at once instead of even considering to produce | value in order to prove _themselves_ as a valuable and | knowledgeable part of the team and not the work they do. So | it 's a win from the start. Especially if you have 400 | people in your team with 54 well-documented gatherings | under their belts. | whycombagator wrote: | Not sure that LOC is a meaningful metric/measure of scale, | unless you mean scale of codebase itself | mandelbrotwurst wrote: | 100 billion? That seems like a lot of businesses per capita! | shwoopdiwoop wrote: | Fairly certain GP referred to the market cap, not the number | of businesses on Shopify's platform. | khendron wrote: | He might be referring to Shopify GMV (Gross Merchandise | Volume --the value of commerce facilitated by the | platform), which is probably approaching $100B per year. | csomar wrote: | No shopify market cap is $100bn; which is much higher | than I expected. So I looked up their revenue, which is | $1.6bn and they have an income deficit. so... | mandelbrotwurst wrote: | Ah, yeah I thought it said businesses plural. | jtsiskin wrote: | "business", not "businesses" | tgarv wrote: | I think "100 billion business" means that the business | (Shopify) is valued at $100 billion. (I'm not sure if that's | true, that's just how I interpreted it) | ryanmarsh wrote: | There's so much truth in this. It's full of lessons I tell | clients at the outset of similar endeavors yet they often do not | heed until they experience the pain first hand. | octernion wrote: | we are actually doing precisely the same thing at instacart | (breaking our 1+ million lines of code monolith into discrete | components, which we call "domains"), and typing the boundaries | and as much of the internals of these domains as possible with | sorbet types. | | this has the benefit of ruby dynamicism (fast development within | domains, you can use all the nice railsy tooling, activerecord, | and all the libraries we've built over the years), with type | safety at the boundaries (we've also put in timeouts, thread | separation, and error handling at the boundaries). | | the additional benefit for using sorbet is that it makes making | typed RPC calls (over twirp or graphql) much easier as you can | introspect the boundaries trivially. | | really cool to see other companies evolving similarly given the | same starting conditions! | IshKebab wrote: | My god I can't imagine a million lines of untyped code. Must be | hell. Presumably you spend all day writing tests? | octernion wrote: | hah, it's not hell but it's not entirely pleasant either. a | _lot_ of that is tests, which is essentially how contracts | and safety is enforced in ruby (at least prior to types). | exterm wrote: | There are quite a few people talking about this kind of stuff | on https://rubymod.slack.com. I can send invites, just DM me on | twitter https://twitter.com/_exterm | geospeck wrote: | > I can send invites, just DM me on twitter | | Seems like DM is closed. Thanks for the great article! | exterm wrote: | oops - sorry. Opened for now. | octernion wrote: | just sent you a note, thank you! | dragosmocrii wrote: | Slightly off topic, but does anyone know if this "component | based" development is what umbrella applications are in Elixir? | ravenstine wrote: | Am I the only one who has a distaste for this phrase | "component based development"? It just seems like a fancy way | of saying object oriented programming without an overarching | design pattern. | aidos wrote: | Sounds like the "components" described above are much | larger than classes. | octernion wrote: | that's correct, at least for us a domain encapsulates | many response types and dozens of different APIs that | wrap various datastores, business logic, etc. | octernion wrote: | we've actually taken the pattern of making the classes | relatively stateless, and explicitly passing around typed | state through these explicit apis. it's not really the same | design pattern and imo conceptually different. | exterm wrote: | It's certainly related. In very general terms, I would say | splitting a Rails app into multiple engines is the same | pattern as umbrella applications. | | However, there are more interesting specifics here about | things like all engines sharing a database, but having | exclusive ownership of tables, as well as splitting HTTP | routing over multiple engines etc. | Arubis wrote: | I think you'll also find a lot of conceptual overlap with | Phoenix Contexts; they'll generally all start as part of the | same monolith/app but are sufficiently discrete that you can | separate them out more easily than the Rails situation in | TFA. | leafboi wrote: | I think it's kind of bad that we have this trend to use hardware | to enforce modularity. If it's a performance issue, sure break it | up into more hardware. If it's just code modularity than by | shifting to microservices you are adding additional complexity of | maintaining multiple services on top of modularizing the system. | In short it's overkill. This whole thing about using hardware to | enforce "developer behavior" is stupid. You can use software to | enforce developer behavior. Your operating system, your | programming language is already "enforcing" developer behavior. | | Additionally, your microservices are hard lines of | modularization. It is very hard to change a module once it's been | materialized because it's hardware. | | If you think about it, almost all lack of modularity comes from | shared mutable variables. Segregate mutability away from the core | logic of your system and the smallest function in your | architecture will become as modular as a microservice. | | Really, any function that is stateless can be moved anywhere at | anytime and used anywhere without fear of it being creating a | permanent foothold in the architectural complexity of the system. | So if the code is getting to structured where you become afraid | of moving things... do this rather than go to microservices. | | >We can more easily onboard new developers to just the parts | immediately relevant to them, instead of the whole monolith. | | Correct me if I'm wrong but don't folders and files and repos do | this? Does this make sense to you that it has to be broken down | into hardware? | | >Instead of running the test suite on the whole application, we | can run it on the smaller subset of components affected by a | change, making the test suite faster and more stable. | | Right because software could never do this in the first place. In | order to test a quarter of my program in an isolated environment | I have to move that quarter of my program onto a whole new | computer. Makes sense. | | >Instead of worrying about the impact on parts of the system we | know less well, we can change a component freely as long as we're | keeping its existing contracts intact, cutting down on feature | implementation time. | | Makes sense because software contracts only exist as http | json/graphql/grpc apis. The below code isn't a software contract | it's only how old people do things: int add(x: | int, y: int) | | Remember as long as that add function doesn't mutate shared state | you know it has zero impact on any part of the system other than | it's output... you can replace it or copy it or use it | anywhere.... this is really all you need to do to improve | modularity of your system. | | Editing it on the other hand could have some issues. There are | other ways to deal with this and simply copying the function, | renaming and editing it is still a good solution. But for some | reason people think the only way to deal with these problems is | to put an entire computer around it as a wall. So whenever I need | some utility function that's located on another system I have to | basically copy it over (along with a million other dependencies) | onto my system and rename it... wait a minute can't I do that | anyway (without copying dependencies) if it was located in the | same system? | | >Again and again we pondered: How should components call each | other? | | I think this is what's tripping most people up. They think DI IOC | and OOP patterns are how you improve modularity. It's not. | Immutable functions are what improves modularity of your program. | The more immutable functions you have and the smaller they are | the more modular your program will be. Segregate IO and mutations | into tiny auxiliary functions away from your core logic which is | composed of pure immutable functions. That's really the only | pattern you need to follow and some languages can enforce this | pattern without the need of "hardware." | | >Circular dependencies are situations where for example component | A depends on component B but component B also depends on | component A. | | I've never seen circular dependencies happen with pure functions. | It's rare in practice. I think it occurs with objects because | when you want one method of an object you have to instantiate | that object which has a bunch of other methods and in turn | dependencies that could be circular to the current object you're | trying to call it from. In essence this kind of thing tends to | happen with exclusively with objects. Don't group one function | with the instantiation of other functions and you'll be fine. | | Still I've seen this issue occur with namespacing when you import | files. Hardware isn't going to segregate this from happening. You | need to structure your dependencies as a tree. | look_lookatme wrote: | >>We can more easily onboard new developers to just the parts | immediately relevant to them, instead of the whole monolith. | | >Correct me if I'm wrong but don't folders and files and repos | do this? Does this make sense to you that it has to be broken | down into hardware? | | This entire post is literally about using folders (directories) | and files to enforce boundaries... | leafboi wrote: | they use the term breaking down a monolith and "architecture" | so from that you can derive that it's literally about using | an entire VM or computer to enforce boundaries. | | Folders and files are used in "monoliths" anyway. Nothing new | to talk about that here. Are you implying that their monolith | is just one big file and they're beginning the process of | breaking that thing down into multiple files and different | folders? | | I don't know about you but that doesn't make any sense to me. | exterm wrote: | Hey Leafboi - I recommend reading the first post in the | series for some background https://engineering.shopify.com/ | blogs/engineering/deconstruc... | | We don't use "hardware" or "VMs" to facilitate modularity. | leafboi wrote: | All right. I'm wrong. Didn't know this. Thanks for | linking. Still can't exactly fault me on that. It's not | easy to find the contextual blog post if this post | doesn't easily say it's part of a series. | | Still though, my expose is still relevant, those are some | hard lines that can easily be gotten rid of if your | functions were immutable and not part of a class. | | Any internal private function is safe to use anywhere in | the system as long as it's not attached to a class and it | doesn't modify shared state. If your systems were | modelled this way there would be no need to really think | about modularization as your subroutines are already | modular. | | For example: class A: def | constructor: //does a bunch of random shit | def someMethodThatMutatesSomething() -> output | class B: def | someOtherFunctionThatNeedsClassA: //cannot | call someMethodThatMutatesSomethingwithout doing "a bunch | of random shit" or even possibly modifying or breaking | something else. Modularity is harder to achieve with this | pattern. | | versus: def | somePureFunctionWithNoSideEffects(input) -> output | | somePureFunctionWithNoSideEffectsabove does not need any | hard lines of protection. There is zero need to use the | antics of "deconstructing a monolith" if you structured | things this way. Functions like this can be exposed | publicly for use by anyone with literally zero issues. | | Shared muteable state and side effects is really the key | thing that breaks modularity. Everyone misses it and | comes up with strange ways to improve modularity by using | "walls" everywhere. It's like cutting my car in half from | left to right with a wall and calling it | "modularization." When you find out that the engine in | front actually needs the gas tank in back then you'll | realize that the wall only produces more problems. | modal-soul wrote: | Just because a function is pure doesn't mean there is | zero-risk in exposing it publicly. You're conflating | complexity in managing state with complexity in managing | domain boundaries. | | A tangled web of function calls can be very confusing to | work with, regardless of purity. | leafboi wrote: | From a purely structural standpoint there is no risk. But | you are talking about something different. You use the | word "confusion." | | Confusion is an organizational issue that can be handled | with social solutions like names, namespaces and things | like that. You can compose functions to form higher order | functions with proper naming to make sense of things. So | for example if you have 30 primitive functions you can | compose smaller components into 10 bigger functions in a | higher layer and expose that as an api. This is more of a | semantical thing as you can still use the lower level | primitives as a library and chain those lower level | functions to achieve the same goal as using the higher | level api, the higher level functions just make it easier | to reason about the complexity. | | Confusion, Semantics and organization is in a sense a | social issue that is solved by social solutions like | proper naming, grouping and composing. I'm not dismissing | these issues (they are important) but I'm saying they are | in a different category. | | Overall though the problem I am addressing is structural. | There are real structural issues that occur if your | functions are not pure. When 4 methods operate on shared | state in a class all four methods become glued together. | You cannot decompose or recompose these functions ever. | They cannot be reused without instantiating all the | baggage that comes with the class. | richardlblair wrote: | I think what's really unfortunate here is you started | pretty pointed in what you were saying, and you've stayed | pointed. It reads as confrontational. | | It's unfortunate because you make a good point. Pure | functions do not get the attention they deserve. However, | no one will read that because you just sound like you're | attacking for no real reason. | | I'm only saying this because if you're this way here | there is a solid chance you're like that in other areas | of your life. What you have to say is important, but if | you approach your conversations this way people won't | listen. | | Why did I take the time to write this? Because sometimes | those closest to us won't give us the feedback we need. | throwaway691999 wrote: | https://news.ycombinator.com/item?id=24509449 no one will | read this because nothing is getting attacked. | leafboi wrote: | Thanks. But this is the internet. I use a bit of | aggression experimentally at times. Overall though, it | sounds confrontational but I'm actually pretty factual | and I never attacked anyone personally, it's all about | the topic and idea. I actually admit when I'm wrong (see | above, and who does that in life and on the internet?). | | What's going on is I'm spending zero energy in attempting | to massage the explanation with fake attempts to be nice. | I'm just telling it like it is. Very few opportunities to | do this in real life except on the internet. | | In the company I work for do I spend time to tell my | coworkers that pure functions are the key to modularity | when classes and design patterns are ingrained in the | culture? Do I tell them that their entire effort to move | to microservices is motivated by hype and is really a | horizontal objective with no actual benefit? No. I don't. | People tend to dismiss things they don't agree with | unless it's aggressively shoved in their face. They | especially don't agree with ideas that go against the | philosophies and and practices and they've been following | for years and years. | | Thus if I'm nice about it, I'm ignored, if I'm vocal and | aggressive about it, I'm heard but it will also hurt my | reputation. It's HN feel free to experiment just don't | try it at work. | | Yeah my attitude isn't the best, but honestly, if I was | nice about it, less people would read this or think about | it. By doing this on the internet I can raise a point | while not ruining my rep. (And I'm not actually | aggressive as there are no personal attacks unless | someone said something personal about me) | | Tell me, in your opinion, how would you get such a point | across in a culture where the opposite is pretty | ingrained? I'm down to try this, I can repost my original | post with the errors corrected and a nicer tone to see | the response. | richardlblair wrote: | I appreciate the point you're trying to make, but the | truth is that you can make factual arguments without | being so aggressive. Whether the aggression is targeted | at a person doesn't really matter. It's unnecessary, | disrespectful, and just feeds into the general toxicity | that plagues our culture. | | > Thus if I'm nice about it, I'm ignored, if I'm vocal | and aggressive about it, I'm heard but it will also hurt | my reputation. | | I think the fact we are talking about your tone and not | your points about functional programming speaks to this | by itself. You weren't heard. You were felt, though. | | > I'm not actually aggressive as there are no personal | attacks | | Aggression without a target is still aggression. If I | aggressively take the recycling out, that aggression is | still experienced by people around me. Probably my | partner, who will inevitable have a little talk to me | about it, lol. | | > Tell me, in your opinion, how would you get such a | point across in a culture where the opposite is pretty | ingrained? | | Engage in an intellectual conversion based off mutual | respect. You will never change someones mind on the spot, | intellectual people will often mull things over for a | while. In the process you may learn a few things | yourself. I've worked in places that excelled at this, | where respectful discourse was promoted. Conversations | revolved around facts, but respect was maintained. | | Sidebar: Shopify doesn't really have microservices. They | have a few services, but they are entire services which | serve an entire business unit. They are the exception. | When I worked there I worked on one such service. What | I'd tell people is if you couldn't start a whole new | company with the service you were building, don't build | it as a service. | leafboi wrote: | I think you missed my point. I'm saying when you aren't | aggressive people tend not to want to intellectually | engage with you. People are emotional creatures and what | doesn't excite them emotionally they don't engage. I'm | saying I used the aggression on purpose for my own ends, | but I caveated by saying that no actual attack occurred. | | I think you need to think deeper than the traditional | "mutual respect" attitude and generally being nice. Not | all great leaders acted this way either. It's very | nuanced and complicated how to get people to change or | listen. The internet is an opportunity to try things out | rather then take the safe uncomplicated "nice" way that | we usually try in the workplace. | | >Engage in an intellectual conversion based off mutual | respect. You will never change someones mind on the spot, | intellectual people will often mull things over for a | while. In the process you may learn a few things | yourself. I've worked in places that excelled at this, | where respectful discourse was promoted. Conversations | revolved around facts, but respect was maintained. | | Right except this is exceedingly rare. Most people do not | act this way. Respect was maintained but the point is | instantly forgotten and dismissed. Likely the respect | covers up actual misunderstanding or disagreement. I find | actual intense arguments open people up to say what they | mean rather than cover up everything in gift wrapping. | | Think about this way. The reason why Trump won the | election is not because he was nice. The complexities of | human relationships goes deeper then just "mutual | respect" There are other ways to make things move. The | internet is often an opportunity for you to try the | alternative methods without much risk. | mperham wrote: | I don't think you need to mansplain architecture to the | blog post author. | leafboi wrote: | You can't talk about modularity without touching on | shared mutable state. Shared mutable state is the | fundamental primitive that eliminates modularity. You get | rid of this, you're entire program is now modular. | | None of the writing really gets deep into this so I | assume the author doesn't know. | | It's not "mansplaining" you social justice warrior. I | don't even know the sex of the author and I don't care. | Don't turn this into some sex based conflict. It's called | explaining, and that's all it is. | | I'm assuming you don't know about it either so I suggest | you read my "explanation" as well. | bori wrote: | I like that they completely dodged the term "microservice" in the | whole post. | exterm wrote: | you should read the first post in the series if you want to | read about microservices. | https://engineering.shopify.com/blogs/engineering/deconstruc... ___________________________________________________________________ (page generated 2020-09-17 23:01 UTC)