When do microservices pay off?
A microservice architecture splits an application into small services that are built, deployed and scaled independently and talk to each other over the network. The benefits are real, but so is the cost: every function call becomes a network request that is slower and can fail.
| Option | Fits when | Main cost |
|---|---|---|
| Plain monolith | Small team, product still being discovered | Code tangles as it grows |
| Modular monolith | A few teams, a reasonably clear domain | Needs discipline at module boundaries |
| Microservices | Independent teams, very different scaling needs | Operational complexity and distributed data |
Our rule of thumb: with fewer than two or three independent teams, and a problem that is merely "there is a lot of code", a modular monolith is almost always cheaper and faster. Microservices exist for "teams cannot deploy independently" or "one part carries 50 times the load of the rest".
How to draw service boundaries
The most common mistake is slicing by technical layer (a database service, an API service, a UI service), which makes every small change touch several services. Cut by business capability instead; in a shop, for example: catalogue, cart and orders, payments, notifications. The "bounded context" idea from domain-driven design says exactly this.
Signs of a good boundary
- Most feature changes touch only one service.
- The service can be deployed without coordinating with another team.
- The service owns its database and nobody else reads it directly.
- If two services talk to each other on every request, they are probably one service.

How services communicate
There are two main patterns. In synchronous communication (REST or gRPC) service A waits for B to answer; it is simple and fits questions whose answer is needed right now. In asynchronous communication (events on a message broker such as RabbitMQ or Kafka), A just announces that something happened and every interested service reacts on its own.
- Synchronous: easier to reason about, but if B fails A fails; chains of services add up latency.
- Asynchronous: services decouple and absorb bursts, but the final outcome arrives later and debugging is harder.
Outside the organisation, clients usually talk to a single API gateway that handles routing, authentication and rate limiting in one place. Version your API contracts from the start; we cover the details in designing scalable APIs, and JWT is a common option for service-to-service authentication.
Data and consistency
Once every service owns its database, the single multi-table transaction is gone. "Place order, reduce stock and charge payment" is now three operations in three services and can fail halfway. Two practical patterns:
- Saga: a sequence of local steps, each with a compensating action. If payment is declined, the order is cancelled and the stock is released.
- Outbox: the event is stored in the same local transaction as the data and a separate process publishes it, so data can never be saved while its event is lost.
-- same transaction: the order and the event that announces it
BEGIN;
INSERT INTO orders (id, user_id, total) VALUES (42, 7, 1500);
INSERT INTO outbox (aggregate_id, type, payload, published_at)
VALUES (42, 'order.created', '{"orderId":42,"total":1500}', NULL);
COMMIT;
-- a worker polls unpublished rows, sends them to the broker, then marks them
SELECT id, type, payload FROM outbox WHERE published_at IS NULL ORDER BY id LIMIT 100;Because events can be delivered twice, consumers must be idempotent: processing the same message again must not change the outcome. For frequent reads you can also keep a read copy in a Redis cache.
Resilience when things fail
In a distributed system "slow" is worse than "down": requests pile up and one slow service stalls the whole chain. Three basic tools:
- Timeouts on every outgoing call, always shorter than the caller's own timeout.
- Retries only for idempotent operations, with exponential backoff and jitter so clients do not all retry at once.
- Circuit breaker: after several consecutive failures, stop calling for a while so the target can recover.
async function callWithRetry(url, { retries = 3, timeoutMs = 800 } = {}) {
for (let attempt = 0; attempt <= retries; attempt++) {
try {
const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
if (res.ok) return await res.json();
if (res.status < 500) throw new Error("client error " + res.status); // do not retry 4xx
} catch (err) {
if (attempt === retries) throw err;
}
// exponential backoff with full jitter: 0..(100ms * 2^attempt)
const delay = Math.random() * 100 * 2 ** attempt;
await new Promise((r) => setTimeout(r, delay));
}
}Deployment and observability
Each service must build and deploy with one command, which is why containers are close to a prerequisite. If you have not started with Docker yet, deploying a project with Docker is a good entry point, and the official Docker documentation has the details.
More important is being able to see the system. One user request passes through five services, and without a correlation ID in the logs and distributed tracing, finding the cause of an error is like searching for a needle in a haystack. We cover this in professional monitoring and logging; set it up before you split off the first service, not after.
Migrating gradually from a monolith
A full rewrite almost always fails. The strangler pattern is safer: put a routing layer in front of the monolith and move capabilities one by one into new services until little or nothing of the old code is left.
- First separate modules inside the monolith with clear boundaries and interfaces.
- Pick a capability that is loosely coupled and heavy or fast-changing, such as notifications or search.
- Run the service next to the monolith and shift traffic over by percentage.
- Move the data, delete the old path, and measure what improved before the next step.
If you are unsure about the boundaries or migration path for your own project, our web development team can review the current architecture and suggest the simplest route.
Frequently asked questions
What is the difference between a monolith and microservices?
A monolith is one unit that is built and deployed together. Microservices are several independent services with separate databases that communicate over the network. A monolith is simpler; microservices give team independence and selective scaling.
Does a small startup need microservices?
Usually not. A small team moves faster with a modular monolith and pays less in operations. Once teams grow or one part needs different scaling, you can extract that module.
Should every microservice have its own database?
Yes, if you want independence. A shared database ties services to table structure and makes independent deployment practically impossible. Separation does not have to mean a separate server; a separate schema or user works as long as direct access by others is forbidden.
How is data kept consistent across microservices?
Usually through eventual consistency: each service runs a local transaction and notifies others with events. The saga pattern undoes failures with compensating actions, and the outbox pattern guarantees that saving data and publishing the event happen together.

Comments
No comments yet. Be the first to share your thoughts.