[HN Gopher] Modular Monoliths Are a Good Idea
___________________________________________________________________
Modular Monoliths Are a Good Idea
Author : riccomini
Score : 61 points
Date : 2024-09-13 19:12 UTC (3 hours ago)
(HTM) web link (materializedview.io)
(TXT) w3m dump (materializedview.io)
| mattnewton wrote:
| As an ex-fang engineer myself, I have never advocated for more
| services, usually have pushed for unifying repos and multiple
| build targets on the same codebase. I am forever chasing the zen
| of google3 the way I remember it.
|
| If anything my sin has been forgetting how much engineering went
| into supporting the monorepo at Google and duo-repo at Facebook
| when advocating for it.
| paperplatter wrote:
| Do FAANG engineers normally advocate for more services instead
| of fewer? I haven't gotten that impression.
| aleksiy123 wrote:
| Smaller services but not necessarily more binaries.
|
| The current direction I think is to build composable services
| that could be run together or separately.
|
| Where a service is a logical grouping of an RPC interface.
|
| Here is some public work in this direction from Google
|
| https://serviceweaver.dev/
| paperplatter wrote:
| Makes sense, since Google uses a monorepo (google3).
| ecshafer wrote:
| There seems to be more interest in building monorepo support
| now. Some tools, start ups, etc. I would bet Github is working
| on increasing support as well for large repos. So I think
| Google was ahead of the curve there.
| recursivecaveat wrote:
| What were the two facebook repos? I can't find any reference to
| them.
| paperplatter wrote:
| "To get similar characteristics from a monolith, developers need:
| Incremental build systems Incremental testing
| frameworks Branch management tooling
| Code isolation enforcement Database isolation
| enforcement"
|
| This sounds a lot like microservices, most of all the last point.
| Is the only difference that you don't use RPCs?
| nine_k wrote:
| > _the only difference that you don 't use RPCs_
|
| But it's a _huge_ difference. No RPC overhead. No lost /
| duplicate PRC messages. All logs can literally go to the same
| file (via e.g. simple syslog).
|
| Local deployment is dead simple, and you can't forget to start
| any service. Prod deployment never needs to handle a mix of
| versions among deployed services.
|
| Beside that, the build step is much simpler. Common libraries'
| versions can never diverge, because there's one copy per the
| whole binary (can be a disadvantage sometimes, too). You can
| attach a debugger and follow the entire chain, even if it
| crosses the boundaries of the modules.
|
| With that, you can make self-contained modules as small is it
| makes logical sense. You can pretty cheaply move pieces of
| functionality from one module to another, if it makes better
| sense. It's trivially easy to factor out common parts into
| another self-contained module.
|
| Still you have all the advantages of fast incremental / partial
| builds, contained dependencies, and some of the advantages of
| isolated / parallel testing. But most importantly, it preserves
| your sanity by limiting the scope of most changes to a single
| module.
| paperplatter wrote:
| There would be a mix of versions, managed via branches.
|
| The part about debuggability sounded appealing at first, but
| if the multiple services you want to run are truly that hard
| to spin up locally, it won't be any easier as a monorepo.
| First thing you'll do is pass in 30 flags for the different
| databases to use. If these were RPCs, you could use some
| common prod or staging instance for things you don't want to
| bother running locally.
| nine_k wrote:
| > _There would be a mix of versions, managed via branches_
|
| "We build the image slated for deployment from the release
| branch which is cut from master daily / weekly at noon."
| Works for MPOW, and some previous places. It's a monolith,
| there are rules!
|
| > _but if the multiple services you want to run are truly
| that hard to spin up locally, it won 't be any easier as a
| monorepo. First thing you'll do is pass in 30 flags for the
| different databases to use._
|
| Agreed! Maye that would be a reason to split the thing
| finally into separate services. Not necessarily _micro_
| -services, just into parts that are self-contained enough.
|
| But most code bases are not nearly as heavyweight. They can
| work pretty well as a monolith, run as a whole on a decent
| laptop, along with a database or two, or even three
| (typically Postgres, Redis, and Elastic). I know because I
| did it many times, and a ton of other people did. Worse
| yet, I ran the whole bunch of microservices, much like
| production, locally, again, as many a developer here did,
| too. At the scale this small, the complexity just slows you
| down.
|
| > _use some common prod or staging instance for things you
| don 't want to bother running locally._
|
| I've seen this in much bigger projects, and there it made
| complete sense. When you have to move literally a ton, it
| makes sense to use a forklift. But if the thing is a stack
| of papers that fits into a backpack, the forklift is an
| unnecessary bulk and expense. It could sill be a _neatly_
| organized stack of papers, not a shapeless wad.
| paperplatter wrote:
| I'd avoid having separate services share a DB. Besides
| the overhead, you get scary hidden dependencies that way.
| If this approach is considered not micro but rather an
| in-between, the article should mention it as an option.
| throwaway984393 wrote:
| I'm nearing greybeard status, so I have to chime in on the "get
| off my lawn" aspect.
|
| There is no one general "good engineering". Everything is
| different. Labels suck because even if you called one thing
| "microservices", or even "monolith of microservices", I can show
| you 10 different ways that can end up. So "modular monolith" is
| just as useless a descriptor; it's too vague.
|
| Outside of the HN echo chamber, good engineering practice has
| been happening for decades. Take open source for example. Many
| different projects exist with many different designs. The common
| thread is that if a project creates some valuable functionality,
| they tend to expose it both at the application layer and library
| layer. They know some external app will want to integrate with
| it, but also they know somebody might want to extend the core
| functionality.
|
| I personally haven't seen that method used at corporations. If
| there are libraries, they're almost always completely independent
| from an application. And because of that, they then become shared
| across many applications. And then they suddenly discover the
| thing open source has been dealing with for decades: dependency.
|
| If you aren't aware, there is an entire universe out there of
| people working solely on managing dependencies so that you, a
| developer or user, can "just" install software into your computer
| and have it magically work. It is fucking hard and complicated
| and necessary. If you've never done packaging for a distro or a
| language (and I mean 250+ hours of it), you won't understand how
| much work it is or how it will affect your own projects.
|
| So yes, there are modular moniliths, and unmodular monoliths, and
| microservices, and libraries, and a whole lot of varied designs
| and use cases. Don't just learn about these by reading trendy
| blog posts on HN. Go find some open source code and examine it.
| Package some annoying ass complex software. Patch a bug and
| release an update. These are practical lessons you can take with
| you when you design for a corporation.
| transpute wrote:
| _> If you aren 't aware, there is an entire universe out there
| of people working solely on managing dependencies so that you,
| a developer or user, can "just" install software into your
| computer and have it magically work. It is fucking hard and
| complicated and necessary. If you've never done packaging for a
| distro or a language (and I mean 250+ hours of it), you won't
| understand how much work it is or how it will affect your own
| projects._
|
| When new employees joined the engineering org at a former
| employer, they were required to spend six months on the
| sustaining team, where they could be assigned customer-
| escalated bugs in any part of the codebase. Under the clock to
| deliver a hotfix for production customers, they would be
| required to learn a new area of the complex codebase, work with
| specialists in that area, develop/test a fix and shepherd it
| through review/approval by senior engineers. Those who survived
| this process could apply to the subsystem team of their choice.
|
| There is much for developers to learn from a period of
| apprenticeship in cross-platform software packaging. Start with
| .deb/.rpm, then image customization, A/B upgrades, stateless
| systems and work up to reproducible builds with Yocto, NixOS,
| Guix. In the next few years, SBOMs (software "bill of
| materials" aka package provenance) will become mandatory in
| some regional and vertical markets. This will either cause a
| reduction in dependencies, or increased attention to software
| supply chain relationships that bring regulatory costs.
| ljm wrote:
| I can't help but feel like the author has taken some fairly
| specific experiences with microservice architecture and drawn a
| set of conclusions that still results in microservices, but in a
| monorepo. There's nothing about microservices that suggests you
| have to go to the trouble of setting up K8s, service meshes,
| individual databases per service, RPC frameworks, and so on. It's
| all cargo culting and all this...infra... simply lines the
| pockets of your cloud provider of choice.
|
| The end result in the context of a monolith reads more like
| domain driven design with a service-oriented approach and for
| most people working in a monolithic service, the amount of
| abstraction you have to layer in to make that make sense is
| liable to cause more trouble than it's worth. For a small, pizza-
| sized team it's probably going to be overkill where more time is
| spent managing the abstraction instead of shipping functionality
| that is easy to remove.
|
| If you're going to pull in something like Bazel or even an epic
| Makefile, and the end result is that you are publishing multiple
| build artifacts as part of your deploy, it's not really a
| monolith any more, it's just a monorepo. Nothing wrong with that
| either; certainly a lot easier to work with compared to bouncing
| around multiple separate repos.
|
| Fundamentally I think that you're just choosing if you want a
| wide codebase or a deep one. If somehow you end up with both at
| the same time then you end up with experiences similar to OP.
| paperplatter wrote:
| I think the assumption here is that "microservices" means each
| team is dealing with lots of services. Sometimes it's like
| that. But if you go by the "one service <=> one database" rule
| of thumb, there will probably be 1-3 services per team. And
| when you want to use other teams' stuff, you'll be thankful
| it's across an RPC. First basic reason is if you don't agree
| with that other team on what language to write in.
|
| It'd really help to see a concrete example of a modular
| monolith compared to the microservice equivalent.
| mushufasa wrote:
| Would Django's concept of an 'app' fit your definition of modular
| monoliths?
|
| https://docs.djangoproject.com/en/5.1/ref/applications/
|
| In a nutshell, each django project is an 'app' and you can
| 'install' multiple apps together. They can come with their own
| database tables + migrations. But all live under the same
| gunicorn and on the same infra, within the same codebase. Many
| Django plugins are setup as an 'app'.
| alganet wrote:
| A good analogy is lacking though. "Modular Monolith" sounds like
| a contradiction. It doesn't help the idea.
|
| It inherits culture from OOP stuff, that abstraction was leaked
| to repositories, then it was leaked to packages, and it's being
| roughly patched together into meaningless buzzwords.
|
| It's no surprise no one understands all of this. I see the react
| folks trying to come up with a chemical analogy (atoms, molecules
| and so on), and the functional guys borrowed from a pretty solid
| mathematical frame of mind.
|
| What is the OOP point of view missing here? Maybe it was a doomed
| analogy from the beginning. Let's not go into biology though,
| that can't do any good.
|
| Spare parts, connectors, moving parts versus passive mechanisms,
| subsystems. Hard separation and soft separation. It's all about
| that when doing component stuff. And it has been figured all out,
| we just keep messing how we frame it for no reason.
| bbor wrote:
| Oo I love a terminological discussion, well said! I would
| disagree on your first point though: aren't most large machines
| modular monoliths? Say, cars, airplanes, and dams? I absolutely
| agree that this usage kinda erases the original intent of the
| word "monolith" in a software context, though. Or at least
| complicates it greatly...
|
| Personally, I'm putting all my money on _cognitive_ , and the
| terms that go along with it - say, _social_ , _agential_ ,
| _discursive_ , and _conversational_. Not to forget the deeper
| cuts from philosophy of mind (the precursor to CS!), such as
| _dialectic_ (a highly-mutable data structure with holistic
| modification functions?), _architectonic_ (code-generators
| built in to the very "top" of a system, breaking it down into a
| binary tree of generated-code-facilitated subsections?), and
| _striated vs smooth_ systems (describes the level of
| obstruction /complication present in each?).
|
| Ultimately my takeaway from this article is that absolutes
| (namely, microservices) rarely work in real world architecture
| contexts. When looked at in those general terms, I think we
| have little choice but to start treating software like minds to
| be "shaped" or "sculpted", to use Minsky's preferred
| terminology. After all, I believe that at the
| end of the century the use of words and general educated
| opinion will have altered so much that one will be able to
| speak of machines thinking without expecting to be
| contradicted. I believe further that no useful purpose is
| served by concealing these beliefs.
|
| - Alan Turing, 1950's _Computing Machinery and Intelligence_
| alganet wrote:
| Isn't OOP supposed to be about bringing everyday human things
| as the paradigm for thinking? Stuff like doors, buttons,
| locks and keys, levers.
|
| There are some good gems in the popular developer culture.
| "Circuit Breaker" is a de-facto name for an OOP pattern I
| enjoy. "Dependency Injection" not so much, could be called
| "Spare Parts Design".
|
| Architectonic styles comes from Christopher Alexander stuff.
| It's a classic, patterns and GoF comes from that. We could
| use some of his later _Nature of Order_ stuff as well (the
| "Order" name I don't like that much though, but it's good
| content nonetheless).
|
| I think this is the most cognitive (in what it does, not in
| what it says) we can get without being too geeky for
| beginners.
|
| Minds to be "educated", maybe? Like we humans do. Small
| everyday stuff first, then more complicated human things
| later.
|
| Anyway, about the "modular monolith": what about we just call
| it a "machine"? We already expect most machines to be
| modular, and the sum of its parts to do more than its
| individual components, and the idea that the components are
| designed and maintaned together.
| andy_ppp wrote:
| Elixir + Phoenix is so great at this with contexts and eventually
| umbrella apps. So easy to make things into apps that receive
| messages and services with a structure. I'm amazed it isn't more
| popular really given it's great at everything from runtime
| analysis to RPC/message passing to things like
| sockets/channels/presence and Live View.
| sethammons wrote:
| the elixir shop I was at, folks just repl'd into prod to do
| work. Batshit insanity to me. Is that the elixir way? Are you
| able to easily lock down all writes and all side effects and be
| purely read only? If so, they never embraced that.
| andrewmutz wrote:
| > repl'd into prod to do work
|
| Like for debugging production problems and fixing customer
| data? Or for normal development?
|
| If its the former that's a great use of technology, and if
| its the latter it sound insane.
| andy_ppp wrote:
| It depends on the situation, if something is broken in a live
| system and you can login and introspect the real thing this
| is awesome. Obviously there are trade offs and potentially
| you might break things further!
| frompdx wrote:
| I've been picking up Elixir and the Pheonix framework and I'm
| impressed so far. Pheonix is a very productive framework. Under
| the hood Elixir is very lisp-like, but the syntax is more
| palatable for developers who are put off by the syntax of Lisp.
|
| Why isn't it more popular? It's always an uphill battle to
| introduce a new programming language or framework if BIGNAME
| doesn't use it.
| gwbas1c wrote:
| > In practice microservices can be just as tough to wrangle as
| monoliths.
|
| What's worse: _Premature scalability_.
|
| I joined one project that failed because the developers spent so
| much time on scalability, without realizing that _some basic
| optimization of their ORM_ would be enough for a single instance
| to scale to handle any predictable load.
|
| Now I'm wrangling a product that has premature scalability. It
| was designed with a lot of loosely coupled services and high
| degrees of flexibility, but it's impossible to understand and
| maintain with a small team. A lot of "cleanup" often results in
| merging modules or cutting out abstraction.
| stephen wrote:
| I mean, of course they are a good idea, what we need is more
| examples of actually doing them in practice. :-)
|
| I.e. quoting from the post:
|
| - monolithic databases need to be broken up - Tables must be
| grouped by module and isolated from other modules - Tables must
| then be migrated to separate schemas - _I am not aware of any
| tools that help detect such boundaries_
|
| Exactly.
|
| For as much press as "modular monoliths" have gotten, breaking up
| a large codebase is cool/fine/whatever--breaking up a large
| _domain model_ is imo the "killer app" of modular monoliths, and
| what we're missing (basically the Rails of modular monoliths).
| bluGill wrote:
| The thing microservices give is an enforced api boundry. OOP
| classes tried to do that with public/private but fail because
| something public for this module is private outside. I've written
| many classes thinking they were for my module only and then
| someone discovered and abused it elsewhere. Now their code is
| tightly coupled to mine in a place I didn't intend to be coupled.
|
| i don't know the answer to this it is just a problem I'm
| fighting.
| Twisol wrote:
| Different languages handle this in different ways, but the most
| common seems to be adding access controls to the class itself,
| rather than just its members.
|
| For instance, Java lets you say "public class" for a class
| visible outside its package, and just "class" otherwise. And if
| you're using Java 11 modules (nobody is though :( ), you can
| choose which packages are exported to consumers of your module.
|
| In a similar vein, Rust has a `pub` access control that can be
| applied to modules, types, functions, and so on. A `pub` symbol
| is accessible outside the current crate; non-pub symbols are
| only accessible within one crate.
|
| Of course, lots of languages don't have anything like this. The
| biggest offender is probably C++, although once its own version
| of modules is widely supported, we'll be able to control access
| somewhat like Java modules and Rust crates, with "partitions"
| serving the role of a (flattened) internal package hierarchy.
| Right now, if you do shared libraries, you can tightly control
| what the linker exports as a global symbol, and therefore
| control what users of your shared library can depend on --
| `-fvisibility=hidden` will be your best friend!
| steveklabnik wrote:
| > A `pub` symbol is accessible outside the current crate;
|
| This is not universally true; it's more that pub makes it
| accessible to the enclosing scope. Wrapping in an extra "mod"
| so that this works in one file: mod foo {
| mod bar { pub fn baz() {
| } } pub fn foo() {
| bar::baz(); } } fn
| main() { // this is okay, because foo can call
| baz foo::foo(); // this is not
| okay, because bar is private, and so even though baz is
| marked pub, its parent module isn't
| foo::bar::baz(); }
| eichi wrote:
| It doesn't matter if all of the team welcome the idea toward
| better productivity and enhance architecture iteratively. Culture
| and talent matters.
| devit wrote:
| Well, that's just the normal way to write software, no?
|
| Aside from some websites and small scripts, all software is
| written like that.
|
| You simply create a hierarchical directory structure where the
| directories correspond to modules and submodules and try to make
| sure that the code is well split and public interfaces are
| minimal.
___________________________________________________________________
(page generated 2024-09-13 23:00 UTC)