Microservices have become the default “modern” architectural choice. The promises sound incredible: independent teams, autonomous deployments, faster iteration, cleaner scaling.
But here’s the truth: microservices shift complexity — they don’t remove it. Without real design rigor and production-grade practices, they can make your life significantly harder.
Let’s break down exactly why — with practical, developer-focused examples.
Complexity Shifts, Not Disappears
In a monolith, a database transaction is atomic. When you break apart services, that same transaction turns into a distributed operation across multiple services.
Example use case: user signup
Create user record
Create billing profile
Send welcome email
In a monolith, this might be a simple transaction. In microservices, you now have three different services coordinating.
Example flow:
What happens if the billing service fails after the user record is created? You need a compensating transaction — there is no simple rollback.
Observability is Mandatory
With 20+ services, you cannot debug the old-fashioned way. You must invest in:
Distributed tracing
Correlated structured logs
Metrics with alerting
Example with OpenTelemetry (Node.js):
This gives you traces so you can follow requests across services and spot latency or failures.
The Network is Your New Bottleneck
Moving from function calls to network calls introduces latency, retries, timeouts, and TLS handshakes.
Example deployment YAML snippet in Kubernetes with timeouts and retries defined:
That config ensures you retry billing calls safely rather than exploding on a transient network hiccup.
Organizational Coordination
Conway’s Law is real: if your team has unclear ownership, your services will mirror that confusion.
Practical DevOps step:
Define a team-per-domain mapping, then enforce service boundaries with clear API contracts.
For example, in a contracts
repository:
Contract-first design gives you a shared definition, ideally in Protobuf, JSON Schema, or OpenAPI, so everyone speaks the same language.
Local Development is Painful
Spinning up 30 containers locally is no joke. Even docker-compose
hits a wall.
What worked for us:
Partial stack mocking
API contract tests
Lightweight test doubles with tools like WireMock
Sample WireMock stub example:
This lets you test locally against fake billing services without running the real thing.
Coordinated Deployment Is Hard
With a monolith, you ship one artifact. With microservices, you coordinate many.
Typical CI/CD resources to manage:
Version tagging per service
Shared libraries version control
Infrastructure dependencies (databases, message queues)
Deployment order
Example GitHub Actions snippet for independent service deploys with semantic versioning:
Versioning and tagging every deployment keeps things traceable and rollbacks predictable.
Real Production Use Case
Imagine you split a customer onboarding flow into three services:
UserService
(creates profile)BillingService
(creates billing account)EmailService
(sends welcome email)
Without careful orchestration, these can break apart:
UserService creates profile
BillingService fails to set up billing
EmailService sends welcome email anyway
What you really need is a coordination pattern, for example a saga orchestration to enforce consistency:
Saga orchestrator pseudo-code example:
That is the reality of microservices: you have to build these orchestration patterns yourself.
Final Thoughts
Microservices can be powerful — they scale, they decouple ownership, and they speed up delivery if you have:
solid observability
robust CI/CD
clear domain contracts
reliable coordination mechanisms
well-defined team boundaries
Otherwise, you’re simply replacing monolith pain with a distributed headache.
In short, microservices are far harder than you think, and they deserve far more discipline than a quick architecture diagram in a slide deck.
NEVER MISS A THING!
Subscribe and get freshly baked articles. Join the community!
Join the newsletter to receive the latest updates in your inbox.