Engineering
Why We Chose a Modular Monolith Over Microservices
Every few months a client tells us they want to "go microservices" before we've even scoped what their product does. It's understandable — the pitch is seductive: independent deploys, teams that don't step on each other, services that scale exactly where they need to. What rarely makes it into the pitch is the operational tax you start paying on day one, long before you see any of those benefits.
The pitch, and what it leaves out
Microservices solve a real problem: coordination overhead in large engineering organizations. When you have 200 engineers across 15 teams, a shared monolith becomes a bottleneck — everyone is fighting over the same deploy pipeline, the same release calendar, the same merge conflicts. Splitting along team boundaries lets each group ship independently.
But that problem only exists at a certain scale. Most of the teams we work with are between 5 and 30 engineers. At that size, microservices don't remove coordination overhead — they relocate it. Instead of merge conflicts, you get distributed tracing gaps. Instead of a slow CI pipeline, you get a service mesh to operate, a dozen independent deploy pipelines to maintain, and integration bugs that only show up in staging because two services drifted out of sync.
Our rule of thumb
We tell clients: if you can't clearly name the team boundary a service split would create, you're not ready for it. Technical boundaries (auth vs. billing vs. notifications) are not the same thing as organizational boundaries, and splitting along technical lines without team ownership just adds network hops without adding autonomy.
- Under 30 engineers: default to a modular monolith unless you have a specific, provable scaling bottleneck.
- 30–100 engineers: start splitting along real team boundaries, one service at a time, driven by actual pain points.
- 100+ engineers: microservices become less of a choice and more of an operational necessity — the coordination cost of a shared codebase now outweighs the distributed systems cost.
How we structure a modular monolith
"Monolith" doesn't have to mean "tangled." The projects we're proudest of are modular monoliths with hard internal boundaries enforced at the code level — separate packages per domain (billing, catalog, fulfillment), no cross-package database access, and a shared kernel for things like auth and logging. Each module can be extracted into its own service later with minimal rework, because the boundary already exists in the code, not just in someone's head.
Concretely, that looks like: a single deployable artifact, a single database (with clear schema ownership per module), internal APIs between modules that mirror what an HTTP API would look like, and a CI pipeline that runs the whole test suite in under 10 minutes. That last point matters more than people think — fast feedback loops are what actually let a team move quickly, not deployment independence.
When we do reach for microservices
We're not dogmatic about this. We've extracted services for clients when there was a genuinely different scaling profile — a video transcoding pipeline that needed GPU instances while the rest of the app ran on standard compute, or a notifications service that needed to handle bursty traffic independently from the core transactional path. The pattern is: extract when there's a real technical or organizational reason, not because a conference talk made it sound inevitable.
The takeaway
Architecture decisions should follow your team's actual shape and your product's actual scaling needs — not the architecture used by companies operating at 100x your scale. A well-structured modular monolith will get most teams further, faster, with less operational overhead than a premature microservices split. When you outgrow it, the boundaries you built in will make the extraction straightforward instead of a multi-quarter rewrite.