Sefism is free for learners. We are here to help you grow, one step at a time.Join for free
Sefism.
system designdevopsdatabases

Microservices Practices That Actually Keep Them Workable

Microservices best practices that matter once you genuinely need services, and the honest case for not splitting too early: business boundaries, data ownership, resilience, idempotency, events, tracing, contract tests and gradual deploys.

Tauseef Fayyaz
Tauseef Fayyaz

Jul 29, 202612 min read

0

The short answer on microservices best practices

Most lists of microservices best practices explain how to run services well and skip the decision that causes the most damage: splitting too early. So my first practice, and the one that saves the most pain, is simple. Do not start with microservices unless you have the problem they solve.

I am a Lead Full Stack Engineer and I have worked on both sides of this: large single applications that were a pleasure to change, and distributed systems where adding one field meant three coordinated deployments. The difference was never the technology. It was whether the boundaries matched the business and whether the team respected them.

This post covers when microservices make sense, and then the practices that actually keep them workable once you have them: boundaries, data ownership, failure handling, idempotency, events, observability, contract testing and deployment. If you are a student, most of this will feel abstract until your first job. Read it anyway, because these ideas appear in every system design conversation you will ever have.

Before the best practices: should you use microservices at all?

A well-structured single application with clear internal modules is the right answer for most teams for a long time. Microservices solve an organisational problem, many teams needing to ship 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.

Martin Fowler made the same observation years ago in MonolithFirst: almost all the successful microservice stories he had heard of started with a monolith that got too big and was broken up, and systems built as microservices from scratch often ended up in serious trouble.

There is a well-known recent example in the other direction too. In 2023 the Prime Video team wrote that they moved a video quality monitoring service from a distributed serverless design into a single application and cut infrastructure costs by around 90%. That was one service, not all of Prime Video, but the lesson holds: distribution has a price, and you should pay it only when it buys you something.

SignalProbably stay a monolithMicroservices may be worth it
Team sizeOne or two teamsMany teams blocked on each other's releases
DeploysEveryone can ship without coordinationReleases queue behind unrelated changes
DomainStill being discoveredStable, well-understood business areas
Scaling needsUniform across the appOne part needs very different scaling or tech
Operations maturityNo tracing, manual deploysCI/CD, observability and on-call already in place

If most of your answers sit in the left column, build a modular monolith: one deployable, with internal boundaries you would be happy to split along later. If you already have services, or genuinely have the organisational problem, the rest of this is what keeps them healthy.

Microservices best practices for boundaries and data

Draw boundaries around business capabilities, not technical layers

The most common bad split is by layer: one service for data access, one for the API, one for the logic. Every real change touches all three, so you now need three coordinated deployments to add a field.

Split by business capability instead: orders, payments, inventory, notifications. The test is whether a typical feature fits inside one service. If ordinary work keeps spanning four services, the boundaries are wrong and no amount of tooling will fix it.

It also helps to remember Conway's law: organisations tend to produce systems that mirror their communication structures. If two teams share a service, expect that service to become a negotiation. Align boundaries with team ownership where you can.

Every service owns its data, and nothing else touches it

One database, or at least one schema, per service, and no other service reads those tables directly. The database per service pattern keeps each service's data private and reachable only through its API.

This is the practice teams break first, because reaching into another service's tables is so much easier than asking properly. The moment it happens, you no longer have two services. You have one system with two pipelines and a shared schema nobody can change safely. If a service needs another's data, it asks through the API, or subscribes to events and keeps its own copy.

The cost is real: cross-service queries and transactions become harder, and you will meet patterns like sagas for multi-step business processes. That cost is exactly why you should only split when you must.

Resilience: assume every call fails

A function call becomes a network call, and network calls fail slowly, which is worse than failing quickly. Four defaults cover most of it:

  • Timeouts on everything. A call with no timeout eventually hangs, and hanging threads take the whole service down. Set them explicitly, per dependency.
  • Retries with backoff and jitter, for the right errors only. Retry a timeout or a 503. Do not retry a 400, because it will be invalid the second time too. AWS's analysis concluded jittered backoff should be a standard approach for remote clients, because uniform retries turn a blip into a stampede.
  • Circuit breakers. After enough failures, stop calling and fail fast. In Fowler's description of the circuit breaker, once the breaker trips, further calls return an error without the protected call being made at all, which stops one slow dependency dragging down everything upstream.
  • Bulkheads. Separate connection and thread pools per dependency, so one saturated downstream cannot consume every resource you have.

I list the same defaults from a single-service angle in sixteen things that break a backend. In a distributed system they stop being good hygiene and become survival.

Make operations safe to repeat

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

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

Give write operations an idempotency key: the client generates a unique identifier, the server records it, and a repeat with the same key returns the original result rather than doing the work twice. Stripe's API is the reference example: it stores the status code and body of the first request for a key and returns the same result for later requests with that key. 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 work does not need to happen before the response, publish an event and let interested services react. The order service records the order and emits OrderPlaced; 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 steps, where the second can fail after the first succeeded. The standard fix is the transactional outbox: write the event into an outbox table in the same transaction as the business data, and have a separate relay publish from that table. Messages are then sent if and only if the transaction commits.

Assume messages arrive twice

The outbox relay can publish duplicates, and most brokers give you at-least-once delivery anyway. So consumers must handle repeats: record what you have processed, or make the operation naturally repeatable. This is idempotency again in a different costume, and it is the most transferable idea in this whole post.

An antique measuring dial with hour and minute counters

Observability: you cannot operate what you cannot see

With one application you read the log. With twenty services you need 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 it, debugging across services is guesswork. Building distributed tracing for a polyglot microservice system is a strong final year project for exactly this reason.
  • Structured logs. JSON with consistent field names across services. Grepping twenty different log formats does not work.
  • The four golden signals per service. The Google SRE book names them: latency, traffic, errors and saturation. Track them consistently, in one place. The full Site Reliability Engineering book is free and worth reading.
  • Health checks that check dependencies, rather than returning 200 unconditionally, so your orchestrator can act on them.

Test the contracts, not just the services

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

Contract testing pins down what each service promises and fails the build when a change breaks a consumer. In consumer-driven contract testing, as the Practical Test Pyramid puts it, the consumers drive the implementation of the contract. Pact is the common open source tool, and it is far cheaper than end-to-end tests spanning every service. I go deeper on test types in five kinds of API test. A small API contract checker for CI is a good way to learn it hands on.

Deploy small and gradually

Each service deploying on its own schedule is the entire point. Use rolling or canary releases, watch the golden signals, and roll back automatically on regression. The mechanics are covered in what happens between your pull request and production.

If deploying one service requires coordinating with three other teams, you have a distributed monolith. It has all the operational cost of microservices and none of the independence, and it is the most common failure mode I see.

Microservices best practices checklist

PracticeWhy it mattersSign you are getting it wrong
Split by business capabilityChanges stay inside one serviceMost features touch several services
Database per serviceServices can change independentlyServices query each other's tables
Timeouts, retries, breakers, bulkheadsFailures stay localOne slow service causes a wider outage
Idempotency keysRetries are safeDuplicate orders or charges after timeouts
Events with an outboxLoose coupling without lost messagesData and events drift out of sync
Idempotent consumersDuplicates are harmlessReprocessing causes double side effects
Tracing and structured logsRequests can be followedIncidents need everyone on a call to debug
Contract testsBreaking changes caught in CIIntegration breaks found in production
Independent, gradual deploysAutonomy and low-risk releasesRelease trains across teams

How to learn microservices properly

Nearly everything above is a response to one fact: the network is unreliable. Services buy you independent deployment and team autonomy, and they cost you consistency, debuggability and a lot of operational work. That trade is worth it when coordination between teams is your bottleneck, and a bad trade otherwise.

To learn it well:

Learn it on Sefism

FAQ

What are the best practices for microservices?

Split by business capability, give each service its own data, put timeouts, retries and circuit breakers on every call, make writes idempotent, use events with an outbox for work that can wait, trace every request, and test contracts between services.

When should you not use microservices?

When you have one or two teams, a domain you are still discovering, or no CI/CD and observability in place. A modular monolith is simpler, cheaper and easier to change in those situations.

Should each microservice have its own database?

Yes. Each service should own its data and expose it only through its API or events. Sharing tables between services recreates the coupling you split to escape.

What is a distributed monolith?

A system split into services that still have to be deployed together or changed in lockstep. It carries the cost of microservices without the independence.

How do microservices communicate?

Synchronously over HTTP or gRPC when the caller needs an answer now, and asynchronously through message queues or event streams when the work can happen later.

Ask your own question

If your situation is different from the ones above, the career questions library has honest answers to the questions students and engineers ask me most.

Want a learning roadmap built around you, written answers to your own questions and a place to discuss your FYP? Create your free Sefism account. Members can also book a one to one session when they want to talk it through.

Stay in touch

  • LinkedIn: where I share roadmaps, career advice and honest takes on hiring. Follow, or message me.
  • X (Twitter): short practical thoughts on engineering and careers.
  • Instagram: quick tips and a more personal side.
  • Sefism Discord: meet other students and engineers working through the same job market and share what is working.
  • Sefism on YouTube: walkthroughs and talks.
system designdevopsdatabases
0

Keep learning

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.

From the blog

All posts
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. Sessions are free for approved Sefism members, and a few slots open each week.

  • 1:1 Mentorship
  • Fresh Graduate Mentorship
  • Career Guidance

Brand deals, promos and partnerships: tauseeffayyaz@gmail.com