Software architecture
Monolith vs Microservices: What Should a Startup Choose?
Why most startups should begin with a well-structured monolith, the signs it is time to split, and how to keep that door open from day one.
Few technical decisions are argued about as hotly as this one. Some early products are slowed down by an architecture built for a company many times their size. Others end up as a tangled codebase where every change feels risky. Both problems are avoidable, and neither is solved by picking the fashionable answer.
This article explains both options in plain terms, what each one really costs, and how to build so that the choice you make now does not trap you later.
What each one actually is
A monolith is one application. All the code — sign-up, payments, search, notifications, the admin screens — is built, tested and deployed together as a single unit, usually talking to one database. When one part needs another, it calls it directly, the way one function calls another inside the same program.
Microservices split that application into several small, separate applications, each responsible for one area of the business: an orders service, a payments service, a notifications service. Each runs on its own, usually owns its own database, and is deployed on its own schedule. When one needs another, it sends a request over the network, through an API or a message queue.
“Monolith” has a worse reputation than it deserves. A monolith is not the same as a mess. What people dread is a tangled one — a “big ball of mud” — where every part reaches into every other, so a change to billing breaks search. That is a design problem, not a deployment problem, and splitting a tangled codebase usually gives you tangled services with network calls in between.
Which brings us to the option that sits in the middle.
Why a modular monolith is the sensible default
A modular monolith is still one application with one deployment. Inside it, though, the code is divided into clear modules along business lines — accounts, orders, billing, catalogue — with firm rules about how they talk to each other. Each module offers a small, deliberate set of functions to the others and keeps everything else private. Each owns its own data. Think of it as the discipline of microservices without the plumbing.
For most startups, this is where we would start, for three reasons.
- You do not yet know where the boundaries are. In the early months your understanding of the problem changes constantly. Inside one codebase, moving a responsibility from one module to another is a refactor, checked by your tests. Between two services, it means changing two APIs, migrating data and coordinating two releases. Getting a boundary wrong is cheap in a monolith and expensive across services.
- Speed of learning matters more than scale. An early product’s biggest risk is building the wrong thing, not falling over under load. One codebase, one pipeline and one database let a small team ship, learn and change direction quickly. Our guide to turning a business idea into an MVP explains why that speed matters so much.
- A well-built single application goes a long way. A monolith can run as several identical copies behind a load balancer, on a well-indexed database, as long as it keeps no user state in memory or on local disk. When limits do appear, they tend to be in one place — a slow query, a heavy report, a bulky background job — and you can fix that place without redesigning everything.
Starting with a monolith does not mean never splitting. It means splitting when you have a reason, using knowledge you did not have on day one.
What microservices really cost
Microservices solve real problems, mostly those of large organisations. The price is complexity, and a startup pays it from day one while the benefits arrive later, if at all.
Network calls instead of function calls
A function call inside one program is fast, and it either completes or fails with an error you see at once. A call to another service crosses a network, so it is slower, and it can time out, fail halfway or arrive twice. Every one needs a timeout, retries that are safe to repeat, and a plan for what the user sees when the other side is down.
Distributed data
In one database, “reserve the stock and create the order” can be a single transaction: either both happen or neither does. When stock and orders live in separate services with separate databases, that guarantee is gone. You have to design for the moment in between — the stock was reserved but the order was never created — with events, retries and steps that undo half-finished work. Reports that were one query now gather data from several places.
Observability
When something goes wrong in a monolith, the whole request runs inside one process, so a single stack trace usually shows what happened. With microservices, a single request can pass through several services, so on top of centralised logs and metrics you need distributed tracing just to answer “what happened to this order?” All of that has to be set up and paid for.
Deployment pipelines and infrastructure
Each service needs its own build, tests, CI/CD pipeline, configuration and monitoring, plus a way to run them all together — usually containers and an orchestrator — and a plan for versioning APIs so one release does not break another service. A change is only safe if it works with the versions of the other services actually running in production.
Team overhead
Somebody has to own every service. In a small team, each engineer ends up looking after several, and a simple feature that touches three services needs three coordinated changes. Time spent on plumbing is time not spent on the product your customers pay for.
Side by side, for a young product, the trade-offs look like this.
| What matters | Modular monolith | Microservices |
|---|---|---|
| Releasing | One build and one deployment. | A build and deployment for every service. |
| Calls between parts | In-process calls: fast, with no network to fail. | Network calls: slower, and any of them can fail. |
| Data | One database, so transactions just work. | Data split across services; consistency is your job. |
| Finding a bug | One process, so a stack trace follows the whole request. | Needs tracing to follow a single request. |
| Scaling | Scale the whole application together, usually enough. | Scale each service on its own. |
| Moving a boundary | A refactor inside one codebase. | Changes to APIs, data and deployments. |
| Best team fit | One team, or a few that coordinate easily. | Several teams that release independently. |
Conway’s law and the size of your team
There is a long-standing observation in software known as Conway’s law: the structure of a system tends to mirror the communication structure of the organisation that builds it.
Microservices work best when they follow this law on purpose. Each service is owned by one team, which can change and release it without waiting for anyone else. That independence is the real prize, and it matters most when several teams would otherwise trip over each other.
Turn that around and the case for a startup is clear. If one small team builds everything, there are no team boundaries to mirror. Many services just create hand-offs between people who already work closely together.
Signs it is time to extract a service
Split when you can name the problem the split solves. These are the signs worth watching for.
- One part needs to scale differently. Video encoding, heavy reporting or an AI feature that needs different hardware. Running that workload separately lets you scale it, and pay for it, on its own.
- One part needs its own release cadence. Something that changes daily is held back by something that must be released with great care. Separating them lets each move at its own pace.
- Teams are getting in each other’s way. Once you have several teams, constant merge conflicts and a crowded release queue suggest a team boundary wants to become a service boundary.
- A clearly bounded domain has settled down. A module whose job has stopped changing, which talks to the rest through a small, stable interface and owns its own data, is a good candidate. If you cannot yet describe what it is responsible for in one sentence, it is not ready.
- It has different reliability or security needs. Something like payment handling may justify tighter isolation, stricter access and its own audit trail.
Keeping the door open from day one
The habits that make a monolith easy to split later are the same ones that make it pleasant to work on now.
- Draw module boundaries along business lines. Organise code by what it does for the business — orders, billing, accounts — rather than by technical layer, with every screen in one folder and every database model in another.
- No shared tables across modules. Each table belongs to exactly one module. If billing needs order data, it asks the orders module; it does not read the orders table directly. Shared tables are one of the most common reasons a monolith becomes hard to split. A separate database schema per module makes the rule easy to check.
- Talk through internal APIs. Each module exposes a small public interface and keeps everything else private. Other modules call that interface, never the internals behind it. Later, swapping an in-process call for a network call is a contained change to the code, because nothing else reached past the interface — though the callers then need the timeouts and fallbacks described above.
- Use events for side effects. When an order is placed, the orders module can announce “order placed” and let notifications react, instead of calling them directly. The same pattern maps onto a message queue if you split later, though queued messages can arrive late or more than once, so the code that handles them must cope with both.
- Write good tests at the boundaries. Tests that check each module through its public interface are your safety net when you move code between modules today, and when you move a module into its own service tomorrow.
- Let the build enforce the rules. Good intentions wear thin under deadline pressure. Most mainstream languages have tools that fail the build when one module reaches into another’s internals. Use them from the start.
None of this needs special infrastructure. It is discipline in how the code is organised, and it sits at the heart of how we approach custom software development: build what you need now, structured so the next step stays cheap.
Splitting gradually: the strangler fig pattern
When the time comes, resist the big rewrite. Rebuilding everything at once means a long stretch with nothing new for customers and a risky switch-over at the end.
The safer route is the strangler fig pattern, named after a fig that grows around a host tree and gradually takes its place. You replace the old system piece by piece while it keeps running.
- Put a routing layer in front. Requests go through a gateway or proxy that can send each one either to the monolith or to a new service.
- Pick one well-bounded piece. Choose the module with the clearest reason to move and the cleanest boundary — ideally one that already owns its data and has an internal API.
- Build the new service alongside. Give it its own data store and move its data across, keeping old and new in step during the changeover where needed.
- Shift traffic gradually. Send a small share of requests, or one group of customers, to the new service first. Watch it, compare results, and switch back if something looks wrong.
- Remove the old code. Once all traffic has moved and the new service has proved itself, delete the module from the monolith. Old code left behind only confuses.
- Repeat only if needed. Plenty of products extract one or two services and keep the rest as a healthy monolith. That is a perfectly good place to end up.
Each step is small and can be checked before the next one starts, which suits short sprints far better than a rewrite does.
Where to start
If you are starting a product today, the advice is simple: build a modular monolith, keep the boundaries clean, and keep the infrastructure as plain as your product allows. Revisit the decision when one of the signs above shows up, not before.
If you already have a system and are unsure whether to split it, map it first: which parts change most, which carry the most load, where the bugs cluster and who works on what. Often the right move is to tidy the boundaries inside the monolith, then extract a service only where there is a clear reason.
Architecture is one of the things worth settling in a short discovery, alongside scope and budget; our article on what custom software development costs explains how that works. If you would like a second opinion on your own plans, get in touch and an engineer will reply within one working day.