system designdevopsdatabases

Microservices Practices That Actually Keep Them Workable

Most microservices advice describes how to run them well and skips the decision that causes the most damage, which is splitting too early. The practices that matter once you genuinely need services: boundaries drawn around business capability, data ownership, failure handling, idempotency, events over synchronous calls, tracing and contract testing.

Tauseef Fayyaz

Tauseef Fayyaz

Aug 19, 20265 min read1 views

Read this before you split anything

Most microservices advice describes how to run them well. Very little of it addresses the decision that causes the most damage, which is splitting too early.

So the first practice, and the one that saves the most pain:

Do not start here. A well structured single application, with clear internal boundaries, is the right answer for most teams for a long time. Services solve an organisational problem, meaning many teams needing to deploy independently, and they solve it by adding a distributed systems problem. If you do not have the first problem, you are buying the second for nothing.

If you already have services, or you genuinely have the organisational problem, the rest of this is what actually keeps them workable.

If you want this material as a structured course rather than a list, Design Gurus covers microservices design patterns with worked examples, and their system design fundamentals course covers the groundwork these practices assume.

Draw the boundaries around the business, not the code

The most common split is by technical layer, meaning a service for the database access, one for the API, one for the logic. This is the worst possible arrangement, because every real change touches all three, and you now need three coordinated deployments to add a field.

Split by business capability instead. Orders. Payments. Inventory. The test is whether a typical change fits inside one service. If your ordinary features keep spanning four services, the boundaries are wrong and no amount of tooling will fix it.

Every service owns its data, and nothing else touches it

One database per service, and no other service reads those tables directly.

This is the practice teams break first, because reaching into another service's tables is so much easier than asking for the data properly. The moment it happens, you no longer have two services. You have one system with two deployment pipelines and a shared schema nobody can change safely.

If a service needs another service's data, it asks through the API, or it subscribes to events and keeps its own copy.

Assume every call fails, because eventually it does

A function call becomes a network call, and network calls fail slowly, which is worse than failing quickly.

Timeouts on everything. A call with no timeout will eventually hang, and hanging threads take the whole service down. Set them explicitly, everywhere.

Retries with backoff, and only for the right errors. Retry a timeout. Do not retry a 400, because it will be invalid the second time too. Add jitter, or every client retries in sync and you have built a stampede.

Circuit breakers. After enough consecutive failures, stop calling and fail fast. This stops a slow dependency turning into an outage across everything upstream of it.

Bulkheads. Separate resource pools per dependency, so one saturated downstream cannot consume every connection you have.

Make operations safe to repeat

This one deserves its own section because it is where the real money is lost.

If a call times out, you do not know whether it ran. Retrying might duplicate it. For a payment, that is a genuine incident.

Give operations an idempotency key: the client generates an identifier, the server records it, and a repeat with the same key returns the original result rather than doing the work twice. Every write endpoint that matters needs this.

Prefer events for anything that can wait

Synchronous calls chain failures. If A calls B calls C and C is slow, A is slow, and a user is watching.

Where the work does not have to happen before the response, publish an event and let interested services react. The order service records the order and emits it; email, analytics and inventory pick it up on their own time.

The trap is the dual write: updating your database and publishing an event as two separate operations, where the second can fail after the first succeeded. Use a transactional outbox, meaning write the event into a table in the same transaction as the data, and publish from that table separately.

Assume messages arrive twice

Almost every queue guarantees at least once delivery. Exactly once is mostly a marketing claim.

So consumers must handle duplicates. Record what you have processed, or make the operation naturally repeatable. This is idempotency again, in a different costume, and it is the single most transferable idea in this article.

An antique measuring dial with hour and minute counters

You cannot operate what you cannot see

With one application you read the log. With twenty services you need to be able to follow one request across all of them.

Trace identifiers. Generate one at the edge, pass it through every call and into every log line. Without this, debugging across services is guesswork.

Structured logs. JSON with consistent field names. Grep across twenty differently formatted log styles does not work.

The four numbers, per service. Error rate, latency, traffic, saturation. Consistently, in one place.

Health endpoints that check dependencies rather than returning 200 unconditionally, so your orchestrator can actually act on them.

Test the contracts, not just the services

Unit tests in each service pass. The system is broken anyway, because one team changed a response field.

Contract testing pins down what each service promises and fails the build when a change breaks it. It is far cheaper than end to end tests spanning everything, and it catches the specific failure that services introduce.

Deploy small and gradually

Each service deploys on its own schedule, which is the entire point of doing this. Use canary or rolling releases, watch the four numbers, and roll back automatically on regression.

If deploying one service requires coordinating with three other teams, you have a distributed monolith. It has the operational cost of microservices and none of the independence.

The honest summary

Nearly all of this is about one thing: the network is unreliable, and every practice above is a response to that.

Services buy you independent deployment and team autonomy. They cost you consistency, debuggability and a large amount of operational work. That trade is worth it when you have enough teams that coordination is the bottleneck, and it is a bad trade otherwise.

If you want the underlying patterns properly rather than as a checklist, Building Microservices is the honest book, Design Gurus covers the same patterns as guided lessons, and our system design resources collect both alongside the rest.

system designdevopsdatabases

Tauseef Fayyaz

Written by Tauseef Fayyaz

Lead Full Stack Engineer & Career Mentor. I lead an engineering team by day and mentor engineers through job hunts, promotions and career switches the rest of the time.


Comments (0)

Comments are closed for now.

No comments yet.

Work with me

Stuck on something specific?

Writing only gets you so far. If you want an answer to your situation rather than the general case, book a session and we will work through it together. Every session is free; a few slots open each week.

Follow along

New writing, resources and project ideas land here first.