The actual definition
A microservice is a service that can be changed, tested, deployed and scaled independently of every other service, owning its own data. Everything else — size, line count, "one thing well" — is folklore. If two services must be released together, they are one service with extra latency.
Where to cut
- By business capability, not by entity. "Pricing", "Fulfilment", "Identity" — not "UserService", "OrderTableService".
- By team ownership. Conway's law is a constraint, not a warning. A service with two owning teams will always be a coordination bottleneck.
- By rate of change. Code that changes together should deploy together. Split at the seams where change frequency differs sharply.
- By data ownership. If two candidate services need transactional writes to the same table, they are one service.
A useful negative test: if implementing a typical feature requires changing three services in lockstep, your boundaries are wrong. Merge them and try again.
Database per service
This is the non-negotiable part. A shared database recreates the coupling you paid to remove: you cannot change a schema, cannot deploy independently, and any service can violate another's invariants.
Consequences you must design for:
- No cross-service joins. Compose in the caller, or maintain a read model fed by events.
- Duplicated data is normal. Orders keeps a copy of the product name and price at purchase time — and that is correct, since it is a historical fact, not a reference.
- Reporting moves out. Analytics reads a warehouse fed by CDC or events, not the operational stores.
Synchronous vs asynchronous
| Style | Use for | Danger |
|---|---|---|
| Sync request-response (HTTP/gRPC) | Queries the caller cannot proceed without | Latency and failure chains; availability multiplies down |
| Async events (pub/sub) | Reactions: notify, index, settle, audit | Invisible control flow, ordering, duplicates |
| Async commands (queue) | Work handed to one owner | Backlog growth, poison messages |
checkout -> pricing -> inventory -> tax each 99.9% => 0.999^4 = 99.6% (~3.5 hours/month of failure) same chain with events for tax + inventory reservation confirmation: checkout depends on pricing only => 99.8%
Rule: use synchronous calls only when the caller genuinely cannot answer without the reply. Everything else is an event.
Consistency without transactions
- Saga. A business transaction becomes a sequence of local transactions with compensating actions on failure (refund, release stock). Choreographed via events for simple flows, orchestrated by a coordinator when the flow has real branching.
- Transactional outbox. Write state and the outgoing event in one local transaction; a relay publishes afterwards. This is how you avoid "saved but never published".
- Idempotent consumers. Brokers deliver at least once. Every handler needs a dedupe key and must be safe to run twice.
- Read models. Denormalised projections built from events serve queries that would otherwise be cross-service joins.
Surviving partial failure
In a monolith, a dependency is either there or the process is dead. In a distributed system every call can be slow, duplicated, reordered or lost — and slow is the dangerous one, because it consumes the caller's resources.
- Timeouts everywhere, shorter than the caller's own budget.
- Retries with jittered backoff, only for idempotent operations.
- Circuit breakers so a dead dependency fails fast instead of queueing.
- Bulkheads so one slow dependency cannot exhaust shared pools.
- Graceful degradation: serve the page without recommendations rather than not at all.
The platform you now need
This is the cost teams underestimate. Before the second service ships you need: CI/CD per service, service discovery, centralised structured logging, distributed tracing with propagated correlation IDs, per-service metrics and SLOs, secret management, contract testing, a schema registry for events, and a way to run a meaningful subset locally.
The distributed monolith
Signs you built the worst of both worlds:
- Services share a database or a schema.
- A release requires deploying several services in a specific order.
- A single user request fans out through six synchronous hops.
- Shared libraries containing domain logic must be version-bumped everywhere at once.
- Local development requires running the entire estate.
The remedy is usually merging services, not adding more infrastructure. Merging is a legitimate, senior architectural move.
Interview framing
Strong answer
"I'd split by capability along team lines: checkout, pricing, inventory, fulfilment, notifications. Each owns its database. Checkout calls pricing synchronously because it cannot complete without a price; everything else is events with a transactional outbox and idempotent consumers. Order placement is a saga with compensating actions for payment and stock reservation. I'd put timeouts, circuit breakers and per-dependency bulkheads on every remote call, and I would not start until tracing and per-service SLOs exist — otherwise we lose the ability to debug faster than we gain the ability to deploy."
Follow-ups
- How do you produce a report that spans four services?
- What happens when the payment event is delivered twice?
- When would you merge two services back together?