Architecture · 6 min read
Microservices, macroservices or a modular monolith: how to size your services
The question isn't "monolith or microservices." It's "what is the smallest architecture that meets our real constraints today, and how cheaply can we change it later?"
Three shapes
- Modular monolith: one deployable application with strict internal module boundaries.
- Macroservices: a handful of coarse-grained services, usually aligned to major business domains.
- Microservices: many small services, each deployed and scaled independently.
What the evidence says
Martin Fowler wrote in "Monolith First" that "almost all the successful microservice stories have started with a monolith that got too big and was broken up," and that in the cases he'd heard of where a system was built as microservices from scratch, it had "ended up in serious trouble." That is one practitioner's observation, not a law, but it matches the cost structure: every service boundary adds network calls, versioned contracts, deployment pipelines and distributed failure modes.
Split when a real constraint appears
| Signal | Suggests |
|---|---|
| One component needs to scale far beyond the rest | Extract that component |
| Separate teams keep blocking each other's releases | Split along team ownership |
| Parts need different release cadence or compliance scope | Isolate the regulated part |
| A part needs a different runtime or language | Extract it behind an API |
| You have under ~10 engineers and one product | Stay modular and monolithic |
The costs people forget
- Operational overhead: each service needs CI/CD, monitoring, alerting and on-call knowledge.
- Data consistency: what was one database transaction becomes a distributed workflow with retries and compensation.
- Testing: integration tests across services are slower and flakier than in-process tests.
- Debugging: one request now spans several logs and traces.
Our default recipe
- Start as a modular monolith with enforced module boundaries (one module per business capability, communication through defined interfaces).
- Use queues and webhooks for the integrations that are already asynchronous.
- Write automated tests at module boundaries so a later split is mechanical.
- Extract a service only when a row in the table above is true, and migrate incrementally with the strangler pattern.
Applying it
This is the architecture step of our digital transformation work. If you're weighing a split, book a call and bring your constraints; we'll tell you honestly if you don't need one.