[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)