Microservices Sound Great Until You Have to Maintain Them

by Arif Ikhsanudin, Backend Developer

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.

Scale Your Backend - Need an Experienced Backend Developer?

We provide backend engineers who join your team as contractors to help build, improve, and scale your backend systems.

We focus on clean backend design, clear documentation, and systems that remain reliable as products grow. Our goal is to strengthen your team and deliver backend systems that are easy to operate and maintain.

We work from our own development environments and support teams across US, EU, and APAC timezones. Our workflow emphasizes documentation and asynchronous collaboration to keep development efficient and focused.

  • Production Backend Experience. Experience building and maintaining backend systems, APIs, and databases used in production.
  • Scalable Architecture. Design backend systems that stay reliable as your product and traffic grow.
  • Contractor Friendly. Flexible engagement for short projects, long-term support, or extra help during releases.
  • Focus on Backend Reliability. Improve API performance, database stability, and overall backend reliability.
  • Documentation-Driven Development. Development guided by clear documentation so teams stay aligned and work efficiently.
  • Domain-Driven Design. Design backend systems around real business processes and product needs.

Tell us about your project

Our offices

  • Copenhagen
    1 Carlsberg Gate
    1260, København, Denmark
  • Magelang
    12 Jalan Bligo
    56485, Magelang, Indonesia

More articles

Java Thread Management — Why ExecutorService Exists and How to Use It Well

Creating threads directly is expensive, uncontrolled, and hard to shut down cleanly. ExecutorService solves all three problems — but its default configurations have tradeoffs that matter in production.

Read more

Why Employee Monitoring Tools Are Not Necessary for Remote Teams

Trust beats tracking. Remote teams thrive on autonomy, not constant surveillance.

Read more

No Sudo, No Tools, No Hope: How Bureaucracy Stops Projects Before They Start

Ever tried to get a project moving and hit nothing but red tape? Sometimes, bureaucracy kills momentum before a single line of code is written.

Read more

The Real Cost of a Backend Hire in Stockholm in 2025 — And the Async Alternative

You budgeted SEK 65K a month for a backend engineer. The actual cost turned out to be closer to SEK 100K once you added everything the job listing didn't mention.

Read more