Microservices Sound Great Until You Have to Maintain Them
by Eric Hanson, Backend Developer at Clean Systems Consulting
The Architecture Looks Great on a Whiteboard
Independent deployability. Technology heterogeneity. Team autonomy. Fault isolation. The pitch writes itself. You draw boxes on a whiteboard, put arrows between them, call each box a "service," and it all makes obvious sense.
Then you ship it.
Three months in, a single user-facing feature requires coordinated changes across four services, three teams, and two deployment pipelines. Your local dev environment needs Docker Compose with eleven containers just to run the thing. A bug that took 20 minutes to find in a monolith now requires correlating logs across six services using a trace ID that someone forgot to propagate through a Feign client header.
None of this means microservices are wrong. It means they carry costs the whiteboard doesn't show — and those costs compound directly with the operational maturity of the team running them.
Distributed Systems Complexity Is Not Optional
When you split a monolith into services, you don't eliminate the complexity of the domain. You trade in-process method calls for network calls, and networks fail in ways that method calls don't.
A service that was previously a library boundary now has:
- Latency — every cross-service call adds network round-trip time. Chain four synchronous calls and you've multiplied your p99 latency before writing a line of business logic.
- Partial failure — service A can be up while service B is down, leaving your system in a state that a monolith could never produce.
- Eventual consistency — if you're using async messaging between services (which you should be, for anything non-trivial), data is out of sync between services by design, and your application code must handle that explicitly.
- Distributed tracing overhead — you need correlation IDs propagated through every HTTP header, every Kafka message, every async thread pool. Miss one and your traces are broken.
None of these are insurmountable. All of them require tooling, discipline, and operational investment that a team running a monolith simply doesn't need.
The Operational Floor Is High
A monolith running on a single VM with a Postgres database has a low operational floor. One server, one process, one log file, one deployment artifact. A junior developer can understand the entire deployment in a day.
A microservices system has a high operational floor. You need, at minimum:
- A service mesh or at least consistent HTTP client configuration (timeouts, retries, circuit breakers) across every service
- Centralized log aggregation — ELK stack, Datadog, Loki, whatever — because
ssh-ing into individual containers is not a debugging strategy - Distributed tracing — Jaeger or Zipkin, wired through OpenTelemetry instrumentation in every service
- A container orchestration platform — Kubernetes in practice, which is itself a system with significant operational complexity
- A secrets management solution — because environment variables injected at deploy time don't scale across 20 services
- A CI/CD pipeline per service, or a monorepo toolchain sophisticated enough to build and deploy selectively
If your team isn't already running most of this, adopting microservices means building the application and the platform simultaneously. That is a significant bet, and many teams underestimate it until they're in the middle of it.
Where the Seams Go Wrong
The hardest part of microservices isn't the services — it's the boundaries between them. Get the decomposition wrong and you end up with services that are too chatty, too coupled, or too coarse-grained to deliver the autonomy you were promised.
The common failure mode is decomposing by technical layer instead of by business domain. You end up with a UserService, a DataService, and a BusinessLogicService that are just a distributed monolith — all the operational overhead of microservices with none of the team autonomy or independent deployability.
Domain-driven design gives you the vocabulary to do this correctly: bounded contexts define where one service ends and another begins, and those boundaries should align with organizational ownership. If two services are always deployed together, they're probably one service. If a single team owns both, they're definitely one service.
The other seam failure is synchronous coupling disguised as microservices. If service A makes a blocking HTTP call to service B on every request, you haven't achieved fault isolation — you've just added latency. When B is slow, A is slow. When B is down, A is down. The services are behaviorally coupled even if they're deployed separately.
The fix is to push cross-service communication toward async messaging for anything that doesn't require an immediate response. With Spring Boot and Kafka, a payment service doesn't need to call an inventory service synchronously — it publishes a PaymentCompleted event and the inventory service reacts:
@Service
public class PaymentEventPublisher {
private final KafkaTemplate<String, PaymentCompletedEvent> kafkaTemplate;
public void publish(Payment payment) {
var event = new PaymentCompletedEvent(
payment.getId(),
payment.getOrderId(),
payment.getAmount(),
Instant.now()
);
kafkaTemplate.send("payment.completed", payment.getOrderId().toString(), event);
}
}
@Service
public class InventoryReservationConsumer {
private final InventoryService inventoryService;
@KafkaListener(topics = "payment.completed", groupId = "inventory-service")
public void onPaymentCompleted(PaymentCompletedEvent event) {
inventoryService.reserveForOrder(event.getOrderId(), event.getAmount());
}
}
Now the inventory service can be down for 20 minutes and orders still complete — they just don't get inventory reservation until the consumer catches up. Whether that tradeoff is acceptable depends entirely on your domain. The point is that you made an explicit choice, not an accidental coupling.
The Staffing Math
Conway's Law isn't a suggestion: your system architecture will mirror your communication structure. Microservices work well when you have teams organized around services — each team owns a service end to end, from code to deployment to on-call.
That requires people. A team that owns a service needs to be able to deploy it independently, monitor it, respond to incidents, and evolve it without coordinating with five other teams on every change. At minimum that's 3–5 engineers per service for any serious availability requirement.
If you have 8 engineers and 12 services, nobody owns anything and everything is everyone's problem. The architecture is working against you.
When to Actually Use Them
Microservices are the right call when:
- Scale requirements differ significantly across components — your image processing pipeline needs different scaling characteristics than your user profile API, and deploying them together forces a bad compromise.
- Team topology demands it — you have genuinely independent teams that would otherwise block each other on a shared codebase.
- Regulatory or security isolation is required — PCI scope, data residency, or audit requirements that are easier to satisfy with hard process boundaries.
- You already have the operational platform — Kubernetes, observability stack, CI/CD per service, on-call rotation that can absorb distributed system incidents.
If none of those are true, a well-structured modular monolith — separate packages per domain, enforced dependency rules with ArchUnit, a single deployment artifact — gives you 80% of the organizational benefit at 20% of the operational cost. You can always extract a service later when you have a concrete reason, not a theoretical one.
The Honest Version of the Tradeoff
Microservices solve real problems. They also introduce real costs that are easy to underestimate when the architecture is still a diagram. The teams that succeed with them tend to have already solved the operational problems before committing to the decomposition — not learned to solve them while simultaneously shipping features.
If you're starting a new system with a team of ten and a deadline in three months, start with a monolith. Not because microservices are bad, but because you'll be too busy keeping the application running to build the platform it needs.
Decompose when the pain of the monolith — deploy coupling, scaling constraints, team blocking — is concrete and measurable. Not before.