
Microservices vs Monolith
Most write-ups frame this as a technology decision: which architecture scales better. That framing skips the actual driver. Microservices exist to let separate teams deploy separate services without coordinating a release. If you do not have separate teams with separate release schedules, you are paying the operational cost of microservices to solve a problem you do not have.
Service boundaries must match team boundaries.
Splitting a system into services does not remove coupling; it relocates it. Two services owned by one team that argues internally about the shared interface still carry that coupling, just moved from a function call to a network call. A service boundary only pays off when it lines up with a real team boundary that can decide and deploy independently. Draw that line wrong and you get a distributed monolith: network latency and failure modes, none of the independence services were meant to buy.

Signal oneWho owns the interface
If one team writes both sides of an API, the API is not really a boundary yet — it is one team's internal contract wearing a network call.
Signal twoWho can deploy without asking
A real boundary means one team can ship its service without a second team reviewing, testing or re-deploying anything on their side.
Signal threeWho gets paged at 2am
On-call ownership reveals boundaries faster than an architecture diagram does — whoever gets paged for a failure is the actual owner of that piece.
Signal fourWhere Conway's Law bites
Systems tend to mirror the communication structure of the organisation that built them, whether or not anyone designed it that way on purpose.
A monolith gets expensive in a specific way.
A well-built monolith is not free at scale; it fails in a predictable pattern. Every team shares one build, one test suite and one deploy pipeline, so a slow test anywhere slows every team's release. As headcount grows, more pull requests compete for the same integration branch, and a bad merge can block deployment for people who never touched the offending code. The failure mode is coordination overhead, not raw performance.
One shared deploy pipeline
Every change rides the same pipeline, so one team's failing test or slow migration can hold up a release for everyone else.
One shared runtime, one blast radius
A memory leak or a runaway query in one module can take down the whole application, not just the feature that caused it.
Merge contention grows with headcount
More engineers writing to the same codebase means more conflicting changes competing for the same integration branch and release window.
The operational surface gets much larger.
Splitting a system multiplies the number of things that can fail independently: services, queues, service discovery, versioned contracts and the network calls between all of them. Each service needs its own deployment pipeline, its own monitoring and its own on-call rotation, or the team drowns trying to run twenty things the way it ran one. Distributed tracing, contract testing and backward-compatible versioning stop being optional and become permanent infrastructure work. None of that is a reason to avoid microservices; it is the price of the independence they buy.
A monitoring stack per service
Twenty services need twenty sets of logs, metrics and alerts stitched together, or failures hide in the gaps between them.
Versioned contracts between teams
Every API between two services becomes a contract someone has to version, test and keep backward-compatible indefinitely.
Network calls replace function calls
A call that used to be instant and reliable inside one process now has to handle latency, retries and partial failure.
A distributed monolith is the worst outcome available.
Splitting a system before the team is split gives you the coordination cost of a monolith and the operational cost of microservices at the same time. Deploys still have to happen together because services share a database or a release calendar, so nobody actually gets independence. Meanwhile every call between two services now crosses a network, adding latency and new failure modes that a single process never had. Teams recognise this pattern only after they have paid for it.
A shared database across services
If two services still read and write the same tables, splitting the code did not split the actual coupling underneath it.
Synchronised release trains
If every service still has to deploy together for anything to work, the split has not bought any real independence yet.
Chatty services standing in for one module
Five services calling each other in sequence for one user action often used to be five functions in one process.
Most teams should start with a monolith.
This is the uncomfortable part of the argument: for most teams, at most stages, a well-structured monolith is the right answer, not a compromise. It ships faster because there is one thing to build, one thing to test and one thing to deploy. It is cheaper to run because there is no service mesh, no distributed tracing bill and no platform team required to keep the lights on. Splitting early trades a problem you have — building the product — for one you probably do not have yet.
Early-stage products
Nobody yet knows which module boundaries will matter, so drawing permanent service boundaries this early is usually a guess dressed as architecture.
Small engineering teams
A team of six does not have the coordination problem microservices exist to solve, no matter how the product is structured.
Unclear domain boundaries
Splitting a domain nobody fully understands yet tends to freeze a wrong guess into infrastructure that is expensive to unwind.
A modular monolith earns most of the benefit.
Between a tangled monolith and a fleet of services sits an option most guides skip: a monolith with real internal module boundaries, enforced in code rather than by a network call. Modules communicate through defined interfaces, own their own data access and can be tested in isolation, but still deploy as one unit. This gets you most of the clarity microservices promise, at a fraction of the operational cost. It is also the easiest starting point to split from later, because the internal seams already exist.
01Step oneDraw boundaries around business capability
Group code by what it does for the business — billing, scheduling, notifications — not by technical layer like controllers or models.
02Step twoEnforce boundaries with code, not convention
A linter rule or a module system that blocks cross-module imports catches drift long before it becomes tangled coupling.
03Step threeGive each module its own data access
No module reaches directly into another module's tables; it asks through an interface, the same discipline a real service would need.
04Step fourWatch which module changes most often
The module that changes fastest and independently of the others is usually the first honest candidate for extraction later.
Four signals that justify extracting a service.
Splitting should follow evidence, not a hunch that the architecture looks impure. These four signals are the ones worth acting on, because each names a real cost the monolith is imposing today, not a theoretical one. If none of them apply yet, the honest move is to keep building inside the boundary you already have. If two or three apply at once, the case for extraction is usually solid.
A team is blocked by another team's release
One team cannot ship without waiting on another team's deploy window, and that wait is now a recurring, measurable delay.
One module needs different scaling
A specific workload — image processing, search indexing — needs hardware or scaling rules the rest of the application does not.
A module needs a different technology
A workload is a genuinely better fit for a different language or runtime than the one the monolith is written in.
Answer these before extracting anything.
A team that cannot answer these clearly is not ready to split, whatever the architecture diagram suggests. Each question forces a decision that a service boundary will make expensive to reverse, so it is worth the friction of asking now. Getting these settled first is what separates a deliberate extraction from a fashionable one.
01Who owns this service after it ships?
Name the team before writing the first line, because an orphaned service is worse than a module nobody enjoys editing.
02What happens if this service is down?
Decide whether the rest of the system degrades gracefully or stops working entirely, before that failure happens in production.
03Is the data ready to be split too?
A service without its own data is not really independent yet; shared tables recreate the coupling the split was meant to remove.
Take the boundary question to your own system.
These signals get you most of the way, but they read differently once someone maps them against your actual codebase and org chart. A short review can confirm whether your current boundaries already match your teams, or where the two have quietly drifted apart.
Scope the boundary decision
A scoping conversation can map your current module or service boundaries against your actual team structure before you commit either way.
See how the architecture work is scoped
Architecture reviews and service extraction are both structured engagements, not a rewrite sold as a single fixed package.
Some builds start as microservices directly
When the team split and the independence requirement already exist, starting directly with services can be the right call.
Read the other guides in this set
Related guides cover the operational side of running services once you have decided to split, including orchestration and deployment.
Questions engineers ask before drawing a service boundary.
01Is Conway's Law a real constraint or just a saying?
It describes a pattern that shows up reliably: systems tend to mirror the communication structure of the organisation that builds them. Ignoring it does not remove the effect, it just means the mirroring happens by accident instead of by design. Teams that design service boundaries around actual reporting lines and communication paths get fewer surprises later.
02Can a monolith scale to a large user base?
Yes, a well-built monolith can serve enormous traffic; scaling databases, caching and horizontal replicas is well understood engineering. The limits that actually bite are usually about team coordination, not request throughput. Plenty of large, high-traffic products run as a single deployable for years before anything forces a split.
03How do I know if two services are actually coupled?
Check whether they can be deployed on different days without a coordinated release. If a change to one routinely requires a change to the other in the same release, the boundary is not real yet. That test is more reliable than any diagram of the two services.
04Is a service mesh required once there is more than one service?
Not immediately, and adding one early is a common source of unnecessary operational overhead. A handful of services can often communicate directly with ordinary retries and timeouts. A mesh earns its cost once the number of services makes that manual work too repetitive to maintain by hand.
05How many services is too many to start with?
Starting with more than a handful of services before a single team needs the independence is a common and expensive mistake. A better start is one deployable with clean internal modules, split later along real team lines as they form. The number that is right for you follows from your team structure, not a target service count.
06Who should make the split-or-stay decision?
The people who will own the resulting services long after the migration, not whoever is most excited about the architecture today. Include whoever currently owns on-call for the affected code, since they will feel any coupling gaps first. A decision made without them tends to get revisited within a year.