Modulithic Architecture
The past decade saw everyone from startups to tech titans jump onto the microservices bandwagon, hoping to untangle themselves from their unmaintainable monoliths. The trend can be traced back to Jeff Bezos’ API memo at Amazon, followed by a wave of innovation by Netflix engineering. As tech companies grew, centralized governance became impractical. They transitioned from viewing their organization as a system to an ecosystem, allowing for greater autonomy and concentrated decision-making within teams, and it paid off.
But then something strange happened. Microservices spread everywhere, but the original intention of aligning architecture with organizational design got lost in the folklore. It became an adopted practice simply because everyone else was doing it, and because it made big promises about real problems developers faced with large monoliths. The problem is that the concept scales down terribly - the smaller the product the more the “micro” part really stings. As the microservice-per-team ratio grows beyond two or three, security patches become a constant game of whack-a-mole. Re-imagining the service boundaries dreamed up long ago becomes a year-long coordinated effort. The effort of integrating new build tools or processes is multiplied by the number of services. Developers are left drowning in maintenance tasks.
But what if there’s a middle way for that balances the benefits and drawbacks of both approaches? We could find a sweet spot that gives us the encapsulation and isolation while keeping the system malleable to the demands of new and changing requirements. We could hang on to team independence and developer autonomy, but ensure enough consistency in the technology stack that developers can work across many components of the system. We can deploy parts of the system in isolation without the nightmare of breaking our system into a giant jigsaw puzzle.
This is the sweet spot that modulithic architectures hit. With better support for modularity and encapsulation in our programming tools we can reap many of the benefits of microservices with minimal overhead. We can scope services to be team-sized, so that a team is responsible for maintaining one or two services at most. This doesn’t mean that we’re just creating a more modular monolith. A modulithic service can be one of many services, but you consciously evaluate where those boundaries are weighed up against the maintenance burden it places on future development.
To achieve this modularity two practices are essential:
- State of the art testing Testcontainers, snapshot testing, shared fixtures, better mocking tools. The Node.js ecosystem is leaps ahead of Java here, but that gap is closing. Fast, repeatable, reliable tests that are easy to read and write give developers the confidence to make changes in a busy repo. The shift away from testing microscopic pieces of code (unless justified by complexity) towards testing the behavior of your API or application is essential too. This is no longer the responsibility of fragile Selenium suites.
- Architecture tests The first developers on a project lay out a folder structure with an architecture in mind, but the structure lives only in their minds and so tends to disintegrate over time as others fail to grasp the vision, a phenomenon called “architecture degradation”. Tools like ArchUnit allow us to codify and enforce these constraints. My side project Maven enforcer plugin base-package-enforcer-rule allows you to use ArchUnit to enforce architecture constraints based on your Maven module structure.
The folder structure of a modulithic project resembles the service structure of a microservices ecosystem:
# Top-level module with a main() method, depends on services/*
app/
# Domain-specific parts of the application
# These would be individual services in a microservices architecture.
services/
orders/
products/
billing/
# Code shared between services/* modules
libs/
audit/
utils/
etc...
What about service-to-service communication? Services that talk across a network have an accessible API and hidden internals. We can achieve the same thing with a common module that exposes the interface for other services.
The services can’t accidentally depend on another’s internals, yet refactoring can be done in an IDE with no cross-team coordination.
The recent launch of the incubating Spring Modulith project sends a strong signal that it’s time to move past the monolith/microservice dichotomy. At first “modulithic” might seem like a buzzword, a way for critics of microservices to quietly revert to a monolith while sounding cutting edge. But I think there’s more to it. It draws on a decade of hard-learned lessons of building projects at scale. Giving this concept a name creates dialogue in a space often overshadowed by one-size-fits-all, hype-driven approaches.