A common middle ground: a modular monolith — one deployable, one database, but internally organized into well-separated modules with clean boundaries (much like the microservices diagram above, minus the network hop). Modules communicate through explicit interfaces rather than reaching directly into each other's internals, and — critically — each module's data access is nominally scoped to that module, even though it physically lives in the same database.
| Monolith (unstructured) | Modular monolith | Microservices | |
|---|---|---|---|
| Deployable units | 1 | 1 | N |
| Databases | 1, accessed freely from anywhere | 1, but access scoped per module by convention | N, one per service, enforced by physical separation |
| Enforcing boundaries | Nothing — any code can call any other code | Code review / module structure / lint rules | The network itself — you physically cannot skip an API and reach into another service's database |
| Independent scaling | No | No | Yes |
| Independent deployment | No | No | Yes |
| Operational overhead | Lowest | Lowest | Highest |
A modular monolith gets most of the maintainability benefit of clean separation — a team can work within their module without stepping on another team's code, and the boundaries make it much clearer where a piece of logic actually belongs — without paying the distributed-systems tax (no network calls between modules, no eventual consistency, no distributed tracing needed to debug a request that never left one process). Its boundaries are enforced by convention and discipline rather than by the network, though, which is real: nothing stops a rushed change from reaching directly into another module's tables the way a genuine service boundary physically would. It's also a much easier system to split into real services later, if and when a specific module actually needs independent scaling or deployment — the module boundaries, if kept honest, are close to the eventual service boundaries.