One deployable unit, or many small ones. The fashionable answer and the right answer are frequently not the same.
A monolith is one application, deployed as one unit. Microservices split that application into independently deployable services that communicate over the network. Microservices dominate conference talks because companies with thousands of engineers need them; most products are not that, and adopting the architecture before the problem arrives means paying its costs — distributed debugging, network failure modes, operational overhead — while getting none of its benefits.
A well-structured monolith — clear module boundaries, one database with disciplined ownership, one deploy pipeline — is the fastest architecture a small team can ship and operate. Every function call is in-process: no network latency, no partial failures, no distributed tracing needed to answer “what happened”. Refactoring across module boundaries is an IDE operation, not a cross-team API negotiation.
The “majestic monolith” isn’t a compromise; for most products under roughly a few dozen engineers it is the optimum. Shopify, GitHub, and Stack Overflow ran (and largely still run) on modular monoliths at extraordinary scale. What kills monoliths is not size but entanglement — modules reaching into each other’s internals until nothing can change safely. Discipline about internal boundaries buys years.
Microservices decouple teams, not just code. When many teams ship to the same codebase and deploys queue behind each other, splitting along team boundaries lets each ship independently. They also allow independent scaling — the image-processing service can run on GPU nodes while the API tier autoscales separately — and independent technology choices where genuinely justified.
The bill: every service boundary is a network call that can fail, retry, or time out. You now need service discovery, distributed tracing, contract versioning, and an on-call rotation that understands emergent behavior between services. Data consistency across services is genuinely hard — sagas and eventual consistency replace database transactions. Teams that split too early spend their innovation budget on plumbing.
Your team is small enough that deploy coordination isn't a real bottleneck.
The domain is still evolving fast and boundaries keep moving.
You'd rather spend engineering time on product than on distributed-systems plumbing.
Multiple teams block each other's deploys on a shared codebase.
One component has genuinely different scaling or availability needs than the rest.
A module's boundary has been stable for a long time — extraction is de-risking, not guessing.
No — an entangled monolith is. A modular monolith with enforced internal boundaries scales further than most teams will ever need, and it keeps the option to extract services later at low cost. Architecture debt comes from missing boundaries, not from deployment topology.
A modular monolith with one or two extracted services where the case is overwhelming — a background job processor, a real-time gateway. You get the isolation where it pays without operating twenty services.
Strangler-fig style: pick the module with the most stable boundary, put an interface in front of it, extract it behind that interface, and repeat only when the next real bottleneck appears. Big-bang rewrites into microservices fail far more often than they succeed.
Often, yes — microservices add coordination overhead that only pays off once you have enough engineers that a shared monolith codebase itself becomes the bottleneck.
Yes — that's usually the safer path. Extract the one component with genuinely different scaling or deployment needs first, rather than rewriting everything into services at once.
Functions, managed container platforms, and Kubernetes compared by cost model, cold starts, and operational load — with a practical decision path.
Read the guideAn honest sizing guide — what Kubernetes really costs a small team, the managed alternatives that cover 90% of cases, and the signals you've outgrown them.
Read the guide