
If your engineering team is under about 20 people, build a modular monolith. Almost without exception.
Between 20 and 50, split only along boundaries that are already causing real pain — a component with genuinely different scaling needs, a compliance boundary, a team that is being blocked by another team's release cadence.
Above 50, microservices usually become an organisational necessity rather than a technical preference, because coordinating a single deployable across eight or more teams costs more than running distributed infrastructure.
The number that decides this is not your user count, your funding stage, or how modern you want the architecture to look. It is how many teams need to deploy independently. Everything else in this guide follows from that.
Both terms have been stretched to the point of near-uselessness in vendor marketing, so precise definitions first.
A single deployable unit. One codebase, one build pipeline, one process running in production. Modules communicate through in-process function calls. There is typically one database, though a monolith can talk to several.
A monolith is not necessarily unstructured. A well-built one has strict internal module boundaries, clear ownership, and dependency rules enforced in the build. The failure mode people associate with monoliths — the tangled ball where everything imports everything — is a big ball of mud, which is a discipline problem, not an architectural style.
Multiple independently deployable services, each owning its own data, communicating over the network via synchronous APIs or asynchronous messages. Each service can be built, tested, deployed and scaled without coordinating with the others.
The critical word is independently. If you have twelve services that must be deployed together because they share a database schema, you do not have microservices. You have a distributed monolith, which combines the operational complexity of microservices with the coupling of a monolith. It is the worst outcome available and it is extremely common.
A modular monolith — one deployable, but with module boundaries enforced as strictly as service boundaries would be. Explicit interfaces between modules. No cross-module database access. Dependency rules checked at build time.
This is the right answer for the large majority of software being built today, and it is discussed far less than either extreme because it is unglamorous and nobody sells a platform for it.
Teams choose microservices to solve a scaling problem when what they have is a code organisation problem.
The reasoning goes: our codebase is hard to work in, changes take too long, deploys are risky, the team keeps stepping on each other. Microservices will fix this by forcing separation.
Splitting a codebase you do not understand into services does not produce clean boundaries. It produces the same tangle with network calls between the pieces — and every call that used to be a function invocation that either worked or threw is now a network request that can time out, retry, partially succeed, or arrive twice.
The diagnostic question: if you cannot draw clean module boundaries inside your monolith, you will not draw clean service boundaries either. The boundary problem is a domain modelling problem. Distribution does not solve it; it makes getting it wrong permanent, because moving a boundary between two services is orders of magnitude harder than moving one between two modules.
Fix the modularity first. If the modules end up genuinely independent, splitting them later is mechanical. If they do not, you have learned something important for a fraction of the cost.
In 1967 Melvin Conway observed that organisations design systems which mirror their own communication structures. Sixty years later it remains the most reliable predictor of architectural outcomes in software, and it is the single most useful lens for this decision.
The practical implication: your architecture will end up matching your team structure whether you plan it or not. So the question is not "which architecture is better" but "which architecture matches how our teams actually need to work."
One team, one deployable. If five to fifteen engineers are all working across the same product, a single deployable is the natural fit. Splitting into services means each engineer works across multiple repos, multiple pipelines, multiple deployment surfaces — all overhead, no benefit, because there was never a coordination problem to solve.
Many teams, many deployables. Once you have six or eight teams that each own a distinct part of the product and need to ship on their own schedule, one deployable becomes a bottleneck. Release trains, coordination meetings, and integration branches appear. That coordination cost is what microservices actually buy you out of.
This is why team size is the threshold, not user count. A product serving ten million users with one team of twelve should almost certainly be a monolith. A product serving fifty thousand users built by nine teams may genuinely need services. Load is solved by horizontal scaling and caching. Coordination cost is not.
The inverse also holds, and it is the trap: adopting microservices does not give you the team autonomy that makes them work. If you split into twelve services but all decisions still route through one architect and one release process, you have paid the entire cost and received none of the benefit.
This is the section that gets skipped in architecture comparisons, and it is where the decision is actually made. These costs are real and they are permanent.
Requirement | Monolith | Microservices |
|---|---|---|
Deployment pipelines | 1 | 1 per service |
Service discovery | Not needed | Required |
API gateway | Optional | Effectively required |
Distributed tracing | Nice to have | Required — you are blind without it |
Centralised logging | Nice to have | Required |
Container orchestration | Optional | Effectively required |
Secrets management | Simple | Per-service |
Local dev environment | Run the app | Run the app, or mock 8 dependencies |
The infrastructure cost, translated to money: for a mid-sized product, moving from a monolith to eight to twelve services typically adds $800 to $3,000 a month in cloud spend for equivalent load — orchestration overhead, service mesh, observability tooling, more instances running at lower individual utilisation. It also adds a platform or DevOps function you may not currently have, which is the larger cost.
The overhead nobody budgets for:
Local development. A new engineer's first day goes from "clone, run, it works" to "clone eight repos, configure a docker-compose, discover three of them need credentials nobody documented."
Cross-service changes. A feature touching three services means three PRs, three reviews, three deploys, and a deployment order that matters.
Debugging. A bug that was a stack trace is now a trace across four services, assuming you have distributed tracing configured. If you don't, it is guesswork.
Testing. Integration testing across service boundaries is meaningfully harder than testing in-process. Contract testing becomes necessary, and it is another system to maintain.
Realistic figure: teams moving to microservices without an existing platform capability typically lose 15 to 25 percent of engineering velocity for six to twelve months. Some of that is permanent overhead; some recovers as tooling matures. On a five-person team that is roughly one engineer's output, gone, in exchange for autonomy you do not yet need at that size.
Data consistency. In a monolith, a transaction spanning three tables is a database transaction. Across three services it is a saga, with compensating actions for every failure path. This is not incrementally harder — it is a different discipline, and it is where most distributed systems bugs live.
Versioning. Every inter-service API is a contract with a consumer you cannot deploy in lockstep. Backward compatibility becomes a permanent constraint on every change.
Partial failure. A monolith is generally up or down. A microservices system has states where checkout works but recommendations don't, or where a retry storm from one service degrades three others. You need circuit breakers, timeouts, bulkheads and backpressure — and you need them before the first production incident, not after.
Genuine reasons to extract a service. If none of these applies, you do not need microservices — you need better modules.
One component needs 40 instances at peak while the rest of the system needs three. Video transcoding, PDF generation, ML inference, bulk import — CPU or memory-hungry work with bursty load.
Extracting it lets you scale it independently and, often, run it on entirely different hardware. This is the cleanest and least contested reason to split, and frequently the only one a mid-sized product ever needs.
Two parts of the system need to ship on genuinely different rhythms. A payments module under change control that ships fortnightly after review, alongside a marketing surface that ships four times a day.
Forcing both through one pipeline means the fast one is throttled and the slow one is rushed. That is a real cost, and splitting removes it.
A component handling cardholder data, protected health information, or anything with a defined regulatory boundary. Isolating it shrinks the audit surface dramatically — a PCI DSS assessment against one small service is a fraction of the work of assessing an entire monolith.
For healthcare and financial services work, this is often the strongest argument on the list, and it justifies extraction well before team size would.
Your product is a Node application but the ML inference path needs Python and a GPU. Or a specific component needs a language runtime with different performance characteristics.
Real, but rarer than claimed. "We want to try Go" is not this. The test is whether the requirement is genuinely unmeetable in the existing stack.
Above roughly 50 engineers, coordination cost on a single deployable starts to exceed the operational cost of distribution. This is Conway's Law asserting itself, and it is the reason large organisations end up with services regardless of what they intended.
A single deployable with boundaries enforced as strictly as if the modules were separate services.
Explicit module boundaries. Each module has a defined public interface. Other modules call only that interface, never internal classes. In Java this is package-private plus module descriptors; in .NET, internal plus assembly boundaries; in TypeScript, path-based lint rules; in Python, import-linter contracts.
No cross-module database access. Each module owns its tables. If the orders module needs customer data, it calls the customer module's interface. It does not join across to the customers table. This single rule is what makes later extraction possible, and it is the one most teams break first.
Dependency rules enforced at build time. Not documented in a wiki — checked by CI, failing the build. ArchUnit, import-linter, dependency-cruiser, eslint-plugin-boundaries. A rule that is not mechanically enforced is a suggestion, and suggestions decay.
Modules deployed together but developed independently. Teams own modules. Nobody edits another team's module internals.
You get the benefits that actually matter — clear ownership, contained blast radius, code you can reason about — without distributed systems complexity.
And critically: if a module later needs to become a service, the work is mechanical. The interface already exists. The data is already owned. You replace in-process calls with network calls and deploy separately. Teams that built modular monoliths and later extracted services describe it as an afternoon per module. Teams that built mud balls describe it as a year.
The strategic point: a modular monolith is not a compromise you settle for. It is the option that keeps both futures open at the lowest cost. Microservices are a one-way door in practice — very few teams successfully consolidate back — so keeping the door open has real value.
Work through these in order. The first clear answer wins.
One team → Modular monolith. Stop here. No further analysis needed.
Two to five teams → Modular monolith, with module ownership mapped to teams.
Six to ten teams → Modular monolith if the deploy pipeline is fast and reliable; selective extraction if it is not.
More than ten → Services, almost certainly. Coordination cost dominates.
If yes, extract that specific component. Not the whole system.
Divergent scaling → extract the hot component. Compliance boundary → extract the regulated component. Different deploy cadence → extract the constrained one.
Selective extraction from a monolith is the most under-used architecture in the industry. You do not have to choose one style for the whole system.
Answer honestly:
Do you have distributed tracing, or would you be adding it?
Do you have someone who owns the deployment platform?
Can you run the full system locally, or would engineers be mocking dependencies?
Do you have on-call coverage that can diagnose a cross-service failure at 2am?
Two or more no's means you are not ready for microservices regardless of what the architecture diagram says. The operational capability is the prerequisite, not a follow-on task.
Price it. Additional cloud spend, platform engineering time, 15 to 25 percent velocity loss during transition.
Then name the benefit in the same units. If the answer is "cleaner architecture," you are buying an aesthetic with real money. If it is "the payments team stops waiting four days for a release slot," that is a number you can defend.
If you have worked through the framework and extraction is genuinely justified, the sequence matters enormously.
The greenfield rewrite of a working monolith into microservices is the most reliably catastrophic project in enterprise software. It takes two to three times the estimate, delivers no user value throughout, and the old system keeps changing while you build the new one.
Use the strangler fig pattern: put a routing layer in front of the monolith, extract one capability at a time behind it, route traffic to the new service, delete the old code. The system works throughout. You can stop at any point and still be better off than when you started.
First: the component with the clearest boundary and lowest coupling. Not the most valuable one, not the most painful one. The easiest one. Your first extraction is where you learn your deployment pipeline, tracing setup, and on-call runbooks. Learn those on something that will not take down checkout.
Good first candidates: notifications, file processing, reporting, search indexing. Things that are asynchronous, have few callers, and fail gracefully.
Bad first candidates: authentication, the core domain entity everything references, anything in the payment path.
The most common failure: extracting a service that still reads the monolith's database. That is not a service, it is a second application sharing a schema. Both must now deploy together whenever the schema changes.
Extraction is not complete until the service owns its data. This is the hard part and it is where most migrations stall.
Distributed tracing, centralised logging, and per-service metrics must be in place before the first extraction, not after the first incident. Debugging a distributed system without tracing is guessing, and you will do it at the worst possible moment.
Things that were free in a monolith and now require deliberate engineering.
Transactions. Anything spanning services needs a saga with compensating actions for every failure path. Budget real design time for this — it is where the subtle bugs live.
Referential integrity. The database can no longer enforce that an order references a real customer. That guarantee moves into application code, or it disappears.
Reporting and analytics. A query joining orders, customers and products was one SQL statement. Now it needs a data pipeline into a warehouse. Teams consistently forget this until finance asks for a report that no longer exists.
Local development. Plan for this on day one. Docker Compose, service virtualisation, or a shared dev environment. Without a plan, onboarding time triples and engineers stop running the full system locally, which means integration problems surface in CI rather than on their machine.
Latency. In-process calls are nanoseconds. Network calls are milliseconds. A request path touching six services accumulates real latency, and chatty boundaries turn one user action into forty internal calls. Boundary design is latency design.
On-call. Someone must be able to diagnose a failure spanning services at 2am. That requires runbooks, tracing and dashboards that did not need to exist before.
Consolidated, since this is the variable that actually decides it.
Engineers | Architecture | Reasoning |
|---|---|---|
1 – 10 | Monolith, modular from the start | Coordination cost is zero. Any distribution is pure overhead. |
10 – 20 | Modular monolith, strict boundaries | Module ownership starts to matter. Enforce it in CI. |
20 – 35 | Modular monolith + selective extraction | Extract only components meeting one of the five forces. |
35 – 50 | Hybrid — monolith core, several services | Deploy coordination is becoming a real cost. |
50+ | Services, with a platform team | Coordination cost now exceeds distributed operations cost. |
The critical caveat: these thresholds assume a fast, reliable deployment pipeline. If a monolith deploy takes 90 minutes and fails a third of the time, coordination pain arrives far earlier — but the fix is the pipeline, not the architecture. Teams routinely spend a year on a microservices migration to solve a problem that a week on CI would have addressed.
Fix the pipeline first. Then re-ask the question. It is the single highest-return piece of advice in this article and the most frequently ignored.
The distributed monolith. Services that must deploy together because they share a database or have circular dependencies. All the operational cost, none of the autonomy. The most common microservices outcome by a wide margin.
The nano-service. A service per entity, or per CRUD table. Twelve services where three would do. Network calls where function calls belong. Usually the result of splitting along data structures rather than business capabilities.
The shared library trap. Common code extracted into a library every service depends on. Now a change to that library requires redeploying everything, and you have reintroduced lockstep deployment through the back door.
Services without teams. Twenty services owned by fifteen engineers means nobody owns anything properly. If you cannot name the team for a service, that service should not exist separately.
The resume-driven split. Choosing microservices because it is expected on a CV or in a board deck. Real, common, and expensive. The counter-question that ends this conversation productively: which of the five forces applies to us right now?
Splitting before understanding the domain. Early-stage products do not know their boundaries yet, because the boundaries emerge from usage. Committing to service boundaries in month three means committing to a domain model you will discover is wrong in month nine — and moving a boundary between services is dramatically harder than moving one between modules.
Almost never at the start. With fewer than 20 engineers there is no coordination problem for microservices to solve, and the operational overhead directly slows down the iteration speed a startup depends on. Build a modular monolith with strict internal boundaries — it keeps the option to extract services later at low cost.
Around 50 engineers, or roughly eight to ten teams needing independent deployment. Below that, selective extraction of specific components is usually better than full adoption. The threshold is about team coordination, not user numbers — a product with millions of users and one team should still be a monolith.
Yes. Monoliths scale horizontally by running more instances behind a load balancer, and most scaling limits are in the database rather than the application layer. Read replicas, caching and query optimisation address far more scaling problems than architectural change does. Scale is rarely the real reason teams split.
A single deployable application with internal module boundaries enforced as strictly as service boundaries — explicit public interfaces, no cross-module database access, and dependency rules checked at build time. It delivers clear ownership and contained blast radius without distributed systems complexity, and makes later extraction mechanical rather than archaeological.
For a mid-sized product, expect $800 to $3,000 a month in additional cloud spend for equivalent load, plus a platform or DevOps capability. Teams without existing platform maturity typically lose 15 to 25 percent of engineering velocity for six to twelve months during transition.
Services that appear independent but must be deployed together — usually because they share a database schema or have circular dependencies. It carries the operational complexity of microservices with the coupling of a monolith, and it is the most common failure mode of microservices adoption.
Use the strangler fig pattern rather than rewriting. Put a routing layer in front of the monolith, extract one capability at a time behind it, route traffic to the new service, then delete the old code. Start with a low-coupling asynchronous component such as notifications or reporting, and ensure distributed tracing is in place before the first extraction.
A modular monolith for the first two to three years in almost every case. SaaS products change shape frequently in early life, and service boundaries committed before the domain is understood become expensive to move. Extract specific components — background processing, tenant provisioning, analytics — as concrete needs appear.
Not automatically, and often the opposite initially. They contain failures better in principle, but introduce network partitions, cascading failures and partial-failure states that do not exist in a monolith. Reliability gains require circuit breakers, timeouts, bulkheads and backpressure, all designed in before the first production incident.
Technically yes, and some well-known teams have consolidated successfully. In practice it is rare, because organisational structure reshapes around services and reversing that is harder than the code change. Treat microservices adoption as close to a one-way door, and price it accordingly.
Serverless changes the deployment and billing model, not the boundary problem. A serverless architecture with poorly chosen boundaries fails the same way a microservices architecture does, with added cold-start latency and vendor coupling. Get the boundaries right first; the runtime is a later decision.
The test is change locality. If a typical feature request touches one module or service, your boundaries match your domain. If it consistently touches four, they do not — you have split along technical layers rather than business capabilities. Track this over a quarter; it is the most reliable signal available.
The monolith-versus-microservices debate is usually framed as a technical question. It is an organisational one.
Microservices solve a coordination problem that appears when many teams need to ship independently. If you do not have that problem, they impose cost with no corresponding benefit — and the cost is not one-time. It is permanent infrastructure, permanent operational complexity, and permanent constraints on how changes flow through your system.
For most teams the honest answer is a modular monolith with boundaries enforced in CI, a fast deployment pipeline, and selective extraction of the two or three components that genuinely need it. That is less exciting than a service mesh diagram. It also ships faster, costs less, and preserves the option to change your mind.
The test that matters: can you name which of the five forces applies to you right now? If not, you are solving a problem you do not have yet.
We build modular monoliths by default and extract services when a specific force justifies it. That position costs us the occasional engagement with a client who came in wanting a microservices diagram, and we would rather have that conversation up front than eighteen months in.
Across 180+ software products delivered in 15+ industries, the pattern is consistent. The systems that aged well had strict boundaries and simple deployment. The ones that became expensive had distributed infrastructure introduced before the team was large enough to need it, or before the domain was understood well enough to draw the boundaries.
Two from our own work:
Enterprise HRMS platform — built as a modular monolith with payroll, leave and compliance as strictly separated modules. Payroll processing was later extracted as a service because it had a genuinely different scaling profile at month-end. One force applied; one service was created.
AI quantity takeoff platform — document ingestion and ML inference were separated from the outset. Both needed different hardware and had bursty load. That is force one, and it justified separation on day one rather than as a migration.
We are rated 4.9 out of 5 from 110 Google reviews and 5.0 on GoodFirms, and we would rather earn the next one by talking you out of an architecture you do not need.
If you are weighing this decision for a real system, book a call with me directly. Bring your team size, deploy frequency, and the specific pain you are trying to solve — that is enough to give you a useful answer in thirty minutes.
You can also read how we structure custom software development engagements and enterprise application builds, or post your requirement for a response within one business day.
Subscribe to the Akoode newsletter for carefully curated insights on AI, digital intelligence, and real-world innovation. Just perspectives that help you think, plan, and build better.