10 ms·
Modular monolith and microservices: Modularity is what matters
- mlhpdx 11mo agoIn modular systems, monolithic or distributed, the nuance of "depends on" is often the crux of complexity. A C++ binary interface dependency is more likely to create work than a string correlated via convention.
- CharlieDigital 11mo agoI find it relatively easy to build "modular monoliths" by starting from "vertical slices"[0] at the outset. Vertical slices being primarily a feature organization strategy scales well pragmatically over time, IME. So when first starting out, you can keep it simple and just one monolith and one unit of deployment, but keep everything separated into their own feature folders with common things in a `shared` folder. When you get further along, it is possible to consider splitting things out by feature teams (if that's what works best) and still keep one runtime (just enabling/disabling different features at startup). The common bits can eventually be moved into a package dependency and referenced. Separate features if they become large enough can also be moved into separate packages, but part of the same monolithic codebase. In some cases, it can be easier still and just ship the entire runtime as-is (without any additional work to enable modularity) and simply route different endpoints (e.g. https://feature1.domain.com https://feature1.domain.com -> node set 1, https://feature2.domain.com https://feature2.domain.com -> node set 2) so you still have the option to monitor and scale the features differently based on their load profile and needs. This works great as long as cold starts are not a big concern (thereby adding a requirement for minimizing package size). I find this particularly easy on AWS, especially when deploying with Copilot CLI[1] because it makes it relatively easy to just route different sub-domains to different target groups. Now you have one singular container image that just gets scaled differently by route (e.g. a high volume feature gets bigger nodes and a dedicated route in Route 53). I find some teams have trouble thinking this way because devs many times are not involved enough in the deploy time considerations. For more involved app-level partitioning of modules, I have a practical example in C#[2] that would work equally well with something like Nest.js (or Elysia or Hono) by simply using environment variables to declare a "feature role" for the instance and dynamically enabling/disabling feature modules. [0] https://www.jimmybogard.com/vertical-slice-architecture/ https://www.jimmybogard.com/vertical-slice-architecture/ [1] https://aws.github.io/copilot-cli/ https://aws.github.io/copilot-cli/ [2] https://github.com/CharlieDigital/dn8-modular-monolith https://github.com/CharlieDigital/dn8-modular-monolith
- bognition 11mo agoHard agree with this article. Split up your application by domains, create public apis between modules, understand your dep tree and keep it clean. The devil in the details is how you pull something like this off. At the end of the day is boils down to how do you enforce that your team does the right thing. You can have a single person that enforces standards with an iron fist, but this doesn't scale. You can teach everyone how this should work, but you're going to experience drift over time as people come and go. Or you can enforce it using technology and automation. In the cases of the first choice, its going to restrict how big your team can get and will end up eating all of the time of your one person. In the case of the second choice, a combination of the tragedy of the commons and regression to mean will degrade the system to spaghetti code. For the third scenario language choice matters here a lot. In Java with multi maven modules you can setup maven to forbid imports of specific module types allowing you to make modules as private/public. In Python you can't do any of this.
- stingraycharles 11mo agoAs with all of these things, it needs to come from the very, very top, otherwise it doesn’t work. In the end, AWS only happened because of Jeff Bezos’ infamous “all intra-team communication now goes over HTTP, no exceptions, or you’re fired”-email. The decision to prefer modules whenever they do the job, and defer only to microservices whenever they don’t, seems like the kind of mantra that needs to come from the CTO and made part of the company culture’s DNA.
- kevstev 11mo agoI think the real win with microservices is that when you are air-gapped between services, it really forces modularity and independence. Almost all the time in monoliths things slowly degrade into tangled dependencies. Once clean interfaces that just required a few specific parameters got lazy at some point and now the entire customer or order (or whatever) object is passed in and oops now the two are coupled. This is of course still possible with a microservices architecture, but the barrier to changing a rest contract/API is usually much higher, and people think a lot more about what is being passed across the interface since that data is going to be sent over a wire. Theoretically there is no difference, but its just far easier to slip when its one codebase and all it takes is someone a little too "LGTM" happy to let it through.
- dennisy 11mo agoThe suggested “_contracts” folder lives at the top level of the modules or inside each feature/module? My guess is it’s a top level folder which shows the cross module deps.
- netbioserror 11mo agoModularity is how we put out some long-running fires at my company. Our old infrastructure, from people who had long-since left, was a cross-cutting mish-mash of monoliths that each tried to do everything. Data analysis to web serving to PDF generation. Written in everything from C to Perl to PHP to VBA. All were supported because different clients depended on different monoliths depending on when they were brought aboard. Basically, each language was intended to serve on whatever walled-garden platform any given employee or client was expecting to use it. They didn't even talk to each other, reimplemented the same algorithms, it was a mess. We spun out the specialized tasks (data analysis and PDF generation key among them) to native-compiled binaries or containerized packages like Gotenberg, started moving data around between modules via JSON, isolated the legacy monoliths to containers, unified on our now-modularized PHP backend, and have been working on updating or replacing any other pieces with new modules that can serve the task better. Our clients and non-engineering employees get antsy, but as a smaller company and a smaller programming team, we simply cannot maintain multiple 20-year-old codebases with near-total overlap. It makes no sense now, it didn't make sense when they were each created.
- drob518 11mo agoIMO, 99.999% of apps should just use monoliths with basic 1+1 redundancy. Unless you’re working at a FANG and require just insane scale, you really don’t need microservices or even lots of modularity. Just keep it simple. If you need more resources, buy a bigger system and scale up, not out. Only consider scaling out when you have exhausted scaling up.
- cedws 11mo agoI want to frame this and put it on my wall. Coming up with a greenfield microservice design with arbitrary responsibilities and intercommunication feels so stupid to me. Why not build the thing as a monolith and split parts out when you actually have scaling problems, instead of solving theoretical problems? Development velocity is going to be way higher and it gives developers a chance to discover problems without having to deal with the mental overhead of shipping N services all at once. The value of fast iteration cannot be overstated. Build the minimum viable version of the project first, then stress test it and break parts out when needed. This is much easier if you write modular code.
- xnx 11mo ago> Why not build the thing as a monolith and split parts out when you actually have scaling problems, instead of solving theoretical problems? I sometimes think programmers are the last people who should be writing software. The personality type that likes writing code is the exact type that likes tinkering around the edge and working on hypotheticals instead of addressing the problem at hand.
- hunter2_ 11mo agoOne solution is to have the senior ones (who have already been-there-done-that and lost interest in those possibly low quality outcomes or low velocities from a business perspective) handle architectural decisions and keep things on the rails by way of writing issue specs that the juniors (with those green personalities) will implement, reviewing their code, mentoring them, etc. -- carefully matching an ideal threshold of autonomy for each programmer which will relax over time.
- barishnamazov 11mo agoI recommend checking out concept design [0] from Daniel Jackson [1]. It provides a framework to enforce modularity while building meaningfully designed software. I have done a bit of work implementing a prototype framework for coding concepts using TypeScript [2], and it has worked beautifully for the Software Design class at MIT Daniel teaches. I think the newest iteration of class this semester uses a different approach to code concepts, but it's still a research space. [0] https://essenceofsoftware.com/tutorials/ https://essenceofsoftware.com/tutorials/ [1] https://people.csail.mit.edu/dnj/ https://people.csail.mit.edu/dnj/ [2] https://61040-fa24.github.io/pages/concept-implementations.html https://61040-fa24.github.io/pages/concept-implementations.h...
- Pxtl 11mo agoImho the web and SQL brought us to the ridiculous point that the most obvious way to "modularize" was to use fully separate isolated apps and servers. This is because web and SQL are both dominant platforms for enterprise and are both allergic to modularization. Everything is global, everything is shared. Isolation and private interfaces are either impossible or late-added afterthoughts on a global-first platform. So faced with this, it made sense that the only way to provide modularization was to use isolated computers that only talk over sluggish HTTP.
- dragonwriter 11mo ago> This is because web and SQL are both dominant platforms for enterprise and are both allergic to modularization. Everything is global, everything is shared. Isolation and private interfaces are either impossible or late-added afterthoughts on a global-first platform. That's not actually true of SQL-based RDBMSs (except embedded ones like SQLite), and hasn't been for longer than the median developer has been alive, but it is probably fairly accurate of the average app developers understanding of SQL.
- ecshafer 11mo agoI feel like this misses too much real world context and caveats. What is the problem with Monoliths? Nothing. Until there is. The problem with monoliths is when you have a million LoC Java application that is on Java 6, and will take months of work to get up to date, take 20 minutes to load on a dev machine, starts to fail because its getting too big for a dev machine to handle, can't bring in any new dependencies because of how old the Java version is, and has an old bespoke Ant + Maven + Jenkins + Bash + Perl build and deploy system that has been built up over the last 30 years. So what do you do? Breaking off pieces of the code into microservices that can run on a new Spring boot and can run on a newer nice IaC set up is an easy win. Sure you basically have a microlith, but it increases your dev velocity. I think Monolith issues are typically a symptom of a few other things: 1. accumulated deferred maintenance and tech debt 2. Inadequate developer tooling 3. Inadequate CICD tooling 4. Rarely scale until you really start to hit the size of like Google, Uber, Facebook, etc.
- stavros 11mo agoIf the monolith takes 20 minutes to load, just wait until you see how long it takes for all the microservices to start up.
- netdevphoenix 11mo agoIndeed! And getting odd behaviour and trying to see what went wrong and takes you ages until your realise that one of the 10 micro services was failing rather than it being an actual bug in the micro service you were playing with. Plus, upgrade and maintenance gets multiplied by 10.
- nine_k 11mo agoI watched 1000-node microservice systems start up. Most nodes start really fast, and most of the system is up in seconds, maybe 15-20 seconds, as the flurry of service registration passes. A few nodes would take inordinate time to start up, apparently because they are unlucky and repeatedly get less CPU, less I/O, more retries on congested links, etc. But you don't need to do this on your dev machine. Nearly a decade ago at GrubHub we already had a setup that allowed to run a few microservices under development locally, while relegating the rest to the staging environment that just runs every microservice, like prod, but in small quantities. A JVM-based microservice used to take, say, 16-20 MiB of RAM; a 50-MiB service was considered a behemoth that may need slimming down. You could run quite a number of 20-MiB containers on a laptop with 16 GiB, along with all your dev setup, some local databases, etc.
- dboreham 11mo agoYes, but no once you factor in Conway. Teams deploy and are responsible for their own running services. The line of demarcation is via TCP connections, not the call stack inside some process owned by 9 teams.
- BinaryIgor 11mo agoUnless you have a lot of teams - like 10 and more - it's quite manageable to work on a solidly modularized monolith. But in general you're right; after your reach certain threshold of teams, it makes sense to have a few, not one, units of deployment
- ReflectedImage 11mo agoSo originally software development used micro-services rather than modules. A lot of software developers get this wrong and think modules were first. They were not. The software developers just grew up during the module fad caused by the personal computer before everything circled back around to micro-services. The purpose of OOP was to replicate the benefits of micro-services in single user environments. A class corresponds to a service type and an object an physical instance of the service. So why did Monoliths/modules fail? Some pretty simple issues, incomplete isolate between the modules, memory corruption and performance issues easily propagate between the "isolated" modules. But the main killer is compile times. Monoliths/module based programs require massive compile times that grow quickly with the size of the program.
- BinaryIgor 11mo agoIf you have each module as independently versioned package, the compile times are not slow, since you only need to compile modules that have changed and modules are provided as compiled dependencies. Each individual module is fast to compile.
- bloppe 11mo agoIs there somewhere I could read more about this take?
- foobarian 11mo agoSounds like some mystical time during mainframe heyday era.
- ReflectedImage 11mo agoOf course: https://www.youtube.com/watch?v=wo84LFzx5nI https://www.youtube.com/watch?v=wo84LFzx5nI
- strken 11mo agoSorry, what? Are you claiming that ENIAC was using microservices? That's a claim that needs some kind of supporting evidence, or at least a better explanation.
- bunderbunder 11mo agoMy annoying critique of this article is that I don't think the author made their case strongly enough. I've discovered that their taxonomy at the opening of the article ("single unit of deployment - monolith, multiple units of deployment - microservices/services") is a bit optimistic. Because, when it comes to making things unnecessarily complicated, human ingenuity knows no limits. I've now worked on a project that somehow managed to be monolithic despite having multiple units of deployment. We weren't good about contract/API change management, so in practice it was rare that you could separately deploy "independent" services. And I've also worked on a project that had a single unit of deployment but was somehow still more microservice-like. Everything was packaged into a single giant Docker image that contained the binaries for all the services. (You'd pick which one a container was running with run-time configuration.) But they were well-modularized and services from different versions of the image could talk to each other just fine, so in practice working on it often felt more like successful microservice implementations in development and production. It's just that getting things from development to production was an unholy nightmare because the CI pipeline for that "mono-image" was such a monstrosity.
- BinaryIgor 11mo agoThanks for the feedback :) I'm working on follow-ups, splitting it into multiple articles where I will try to elaborate more on various dimensions of modular monolith vs (micro)services debate/issue/decision and their various tradeoffs. Will try to make it clearer there!
- bunderbunder 11mo agoI realize looking back that my opening sentence isn't quite right. I didn't mean to say that your article wasn't convincing. I meant that I thought that an even bolder version of your premise might also be true. The article is great as-is.
- mdhb 11mo agoI always thought Google had some interesting insights and ideas when they were playing around with the now abandoned service weaver project: https://opensource.googleblog.com/2023/03/introducing-service-weaver-framework-for-writing-distributed-applications.html https://opensource.googleblog.com/2023/03/introducing-servic...? More academic and language neutral introduction here if that’s your thing: https://arxiv.org/pdf/2404.09357 https://arxiv.org/pdf/2404.09357
- asimpletune 11mo agoThe real thing that forces one to choose micro services over modules is when data isolation is a requirement, e.g. security. Capabilities based programming could come along way though to help with closing that gap.
- deleted 11mo ago[deleted]
- kstrauser 11mo agoI feel like there are competent, competing visions talking past each other on the subject. There's kind of a spectrum: 1. Everything is a monolith. Frontend, backend, dataplane, processing, whatever: it's all one giant, tightly coupled vertically-scaled ball of mud. (This is insane.) 2. Everything is a monolith, but parts are horizontally scaled. Imagine a big Flask app where there are M frontend servers, and N backend async task queue processors, all running the same codebase but with different configurations for each kind of deployment. (This is perfectly reasonable.) 3. There are a small number of separate services. That frontend Flask server talks to a Go or Rust or Node or whatever backend, each appropriate to the task at hand. (This is perfectly reasonable.) 4. Everything is a separate service. There are N engineers and N+50% servers written in N languages, and a web page load hits 8 different internal servers that do 12 different things. The site currently handles 23 requests per day, but it's meant to vertically scale to Google size once it becomes popular. Also, everything is behind a single load balancer, but the principle engineer (who interned at Netflix) handwaves it away a "basically infinitely scalable". (This is insane.) These conversations seem to devolve into fans of 1 and 4 arguing that the other is wrong. People in 2 and 3 make eye contact with each other, shrug, and get back to making money.
- 383toast 11mo agoyou can get pretty far with (2) and (3), haven't really understood the need for (4) unless you're FAANG
- yearolinuxdsktp 11mo agoFacts! #1 is not insane as long as you keep your internals modular (all-in-one deployment doesn't mean ball of mud... you can avoid putting service calls into your domain objects or your data plane code). And you can go from #1 to #2 once you see the need and slice the services out that need it (such as decoupling async batch processing into a 2nd service that shares the domain and the data plane code and does not include the front-end).
- kstrauser 11mo ago
- efitz 11mo agoMicroservices embody a concepts that the article overlooks or downplays: 1. modularity - yes - but even better than what the article describes, there is little or no ability to cheat the modularity - the microservice has an api as a contract and is isolated in execution so there’s no trivial way to go around the api. 2. Independently committable/deployable. One reason to consider microservices is organizational- maybe you don’t want to or can’t share a repro with another team. Now of course microservices have lots of downsides and are not a panacea and may be a bad fit for your project.
- bullen 11mo agoIt's about the database, you need to make your own database! As long as your bottom dependency is fixed, you cannot progress! http://root.rupy.se http://root.rupy.se
- twodave 11mo agoI think if you build modules expecting the dependencies to not eventually include most other modules then you either have a trivial application or you’re one PM monkey wrench away from a bad time.
- simianwords 11mo agoi keep seeing "but this is only relevant if you are working at FAANG scale" but most productive applications work at similar scale. there's little fun or education just speaking about mocks or prototypes. sure there are a lot of startups that start off and don't have much traffic. but i don't like that we dismiss real world _productive_ applications because not many companies hit that scale. but most productive applications hit that scale!!
- default-kramer 11mo ago> most productive applications work at similar scale What do you mean by "productive" here? The overwhelming majority (probably >99%) of billed/salaried software development hours are not spent working on FAANG-scale software. Does none of that count as "productive"?
- simianwords 11mo agoYes but the vast majority of applications that make money doing productive things have scale and complexity high enough that a monolith with sql won’t cut it.
- default-kramer 11mo agoI think you're underestimating the huge variety of productive apps in existence. For every system that handles >1M requests per second, there are probably at least 10 systems that won't even see 1M requests per hour. For example: Twice I've worked on apps for configuring motor control centers. I think you would consider these "productive" apps, but even if we had 100% market share there just aren't that many people in the world who need to configure a motor control center on any given day. The world is full of such apps.
- simianwords 11mo agoPeople using and suggesting service oriented architecture are concerned with not just scale in terms of rps but also complexity in terms of lines of code and how much the code changed. The number of apps that are also productive, also low rps, also not that complex, also not dynamic are few in number. I guess this website is such an example.
- hamonrye 11mo ago[dead]
- tuhgdetzhh 11mo agoModular monolith is a pipe dream. Great idea, but it gets messed up in reality (similar to communism). You just need a bit of time pressure and project deadlines and the devs will crack and throw the modularity over board with just a few merges. Multiply that over a decade, then you end up with a monster and fight dragons.
- 4ndrewl 11mo agoI mean, it's stood the (multi-decade) test of time, but it's how Drupal works. Modularised components which interact with a common core. Not perfect, but gives you plenty of real world intel about the benefits, pitfalls and environment that you need to make it work.
- 0xbadcafebee 11mo agoUnfortunately this is not very useful. 'Modularity' is not a guarantee of good software design or operational practice. You can develop a "module" that is tightly integrated with other "modules", making it unusable without the other "modules". This means you have one software component that's effectively split into two, and at least one is unusable on its own. This creates dependency issues, and design issues, as now the second component needs to closely track development of the first component. What's the point of the second module? None from the application's perspective; but it helps with Conway's Law (the worst reason for a software design decision). In addition, "deployment" is not a singular concept. How is a monolith a "single unit of deployment" if it depends on a database with a changing schema? You have to deploy schema changes intentionally, and typically first, but with code that is backwards/forwards compatible. What's a deployment then? Is it a multiple stage process with multiple components? Or is the monolith somehow a "single unit of deployment", even though it entirely depends on the database which has its own "single unit of development", with the first being dependent on the second? (the answer is: a 'deployment' isn't a fixed concept; there is merely systems, and their components, and an order of operations to make changes without impairing system function) Good system design depends on other, more specific concepts, like loose coupling, high cohesion, single responsibility, encapsulation, backwards-compatibility, immutability, versioning, and change management, to name a few. Merely making modules doesn't mean much; monoliths (like microservices), if not mindful, meander into mudballs. It's also an overstatement to claim that microservices are inherently more complex than monoliths. You can easily end up with an overcomplicated monolith too. In fact you can easily end up with an overcomplicated anything. The difference with microservices is you're not [supposed to] make assumptions about data relationships, and you do have to preserve whatever functionality you expose that others depend on. And please remember that monoliths and microservices aren't competing, as if one is supposed to be better than the other. They are just different, like apples and oranges. They have specific [and different] design and function implications. Use them properly as needed. You should be mixing them, not just making one thing in one way as if your whole company must do all things "the one twue way". (Examples [not canonical]: dhcpd is a microservice; dns is a collection of microservices; your browser is a monolith; a web app may be a microservice or monolith (or both); etc)
- lunias 11mo agoThis is exactly what I see all the time in the wild: - users service - quotes service depends on users service - notes service depends on quotes service So... notes depends on users. Unless I'm missing something, I would not distribute this example ever in a million years. Most requests probably pull back all three pieces of data, so just do it in a single query. Do not introduce unnecessary extra network latency to aggregate the data. If you want to scale this horizontally then just add a load balancer in front of n copies of the single service. Now, let's look at an example you might want to distribute: - web service (API) - video encode service (Running on specialized hardware) Now we have an actual reason to separate our services, we want to take advantage of specialized hardware for our long-running, async video encode tasks while leaving our standard synchronous API responsive. Another reason to distribute: - Team A works on API A - Team B works on API B This allows each team to reduce the scope of their knowledge by focusing on a single API, but comes at the expense of extra network calls, serde, API negotiation, and versioning. It cannot be overstated the convenience of: Jump to code definition, update function, compile, fix compilation errors, code works.