Service Decomposition Anti-Patterns#

Splitting a monolith into microservices is a common architectural goal. But bad decomposition creates systems that are harder to operate than the monolith they replaced. These anti-patterns are disturbingly common and often unrecognized until the team is deep in operational pain.

The Distributed Monolith#

The distributed monolith looks like microservices from the outside – separate repositories, separate deployments, separate CI pipelines – but behaves like a monolith at runtime. Services cannot be deployed independently because they are tightly coupled.

Symptoms#

  • Lockstep deployments. Changing Service A requires simultaneously deploying Service B and Service C. Teams coordinate “release trains” instead of deploying independently.
  • Shared libraries with business logic. A common-models library that every service depends on. Updating a model requires updating and redeploying every consumer.
  • Synchronous call chains. Service A calls B, which calls C, which calls D. A request fails if any service in the chain is down. You have the latency of a distributed system with the availability characteristics of a monolith.
  • Shared CI/CD pipelines. A change in one service triggers builds and deployments across multiple services.

Root Cause#

Services were split along technical boundaries (API layer, business logic layer, data access layer) instead of business domain boundaries. Or the original monolith was split too aggressively without redesigning the communication patterns.

How to Fix#

Identify the coupling points. Trace cross-service calls with a distributed tracing tool (Jaeger, Zipkin). Look for services that always deploy together – they should probably be one service. Introduce asynchronous communication (events) to replace synchronous chains where the caller does not need an immediate response.

Nano-Services#

Nano-services are services that are too small to justify independent deployment. Each handles a trivial amount of logic but carries the full operational overhead of a microservice: its own database, CI pipeline, monitoring, on-call rotation, and deployment configuration.

Symptoms#

  • Services with fewer than 500 lines of code. A “string formatting service” or “email validation service” should be a library, not a service.
  • High ratio of infrastructure code to business logic. More Kubernetes YAML, Dockerfiles, and CI config than actual application code.
  • Excessive inter-service communication. A single user request traverses 10+ services because each does one tiny thing.
  • Team cannot keep up with operational overhead. More time spent on deployment pipelines and debugging distributed failures than on building features.

The Test#

Ask: “Does this service need to be independently deployable? Does it scale differently from its neighbors? Is it owned by a different team?” If all three answers are no, it should not be a separate service.

How to Fix#

Merge related nano-services into coarser-grained services aligned with bounded contexts. A “user profile service,” “user preferences service,” and “user settings service” should almost always be a single user service.

Shared Database Coupling#

Two or more services reading from and writing to the same database tables. This is the most common form of hidden coupling.

Why It Happens#

The team splits the monolith’s codebase into separate services but keeps the shared database because migrating data is hard. “We’ll split the database later.” They never do.

What Goes Wrong#

┌──────────────┐     ┌──────────────┐
│ Order Service │     │ Billing Svc  │
└──────┬───────┘     └──────┬───────┘
       │                     │
       └──────────┬──────────┘
                  │
          ┌───────┴────────┐
          │  Shared DB     │
          │  orders table  │
          │  customers tbl │
          └────────────────┘
  • Schema changes break both services. Adding a column to the orders table requires coordinating changes in both the Order Service and the Billing Service.
  • No independent deployability. Database migrations must be compatible with both services simultaneously.
  • Implicit coupling through data. The Billing Service reads order data that the Order Service “owns.” Neither team fully controls the schema.
  • Performance coupling. A heavy query from the Billing Service locks rows the Order Service needs.

How to Fix#

Each service gets its own database (or at least its own schema). Services access each other’s data through APIs or events, not through shared tables. Use the strangler fig pattern: create APIs in the owning service, migrate the other service to use the API, then move the tables.

Synchronous Dependency Chains#

Service A synchronously calls B, which synchronously calls C, which synchronously calls D.

User → A → B → C → D
       ↑              │
       └──────────────┘
       Total latency: sum of all calls
       Availability: product of all availabilities

If each service has 99.9% availability, a chain of 4 services has 99.6% availability – almost 10x more downtime. Latency is the sum of all calls plus network overhead. One slow service makes the entire chain slow.

How to Fix#

  • Cache aggressively. Service A caches responses from B. Stale data for 30 seconds is often acceptable.
  • Use asynchronous communication. Service A publishes an event. Services B, C, D process independently. A does not wait.
  • Introduce bulk/batch patterns. Instead of calling Service B once per request, call it with a batch of 100 items and cache the results.
  • Collapse the chain. If A always calls B which always calls C, maybe A should call C directly. Or B and C should be one service.

Entity Services (CRUD Microservices)#

Entity services map 1:1 to database tables. A Customer Service, an Order Service, an OrderLineItem Service, a Product Service, an Address Service – each wrapping CRUD operations on a single table.

Why This Is Wrong#

Entity services have no business logic. They are database tables with HTTP endpoints. All business logic – “can this customer place this order given their credit limit and inventory?” – lives in an orchestration layer that calls multiple entity services. You have recreated the anemic domain model anti-pattern across a distributed system.

// This is NOT a microservice. This is a database table with an HTTP wrapper.
class CustomerService:
    def get(id): return db.customers.find(id)
    def create(data): return db.customers.insert(data)
    def update(id, data): return db.customers.update(id, data)
    def delete(id): return db.customers.delete(id)

What to Do Instead#

Build services around business capabilities: an Order Management service that handles the full order lifecycle (creation, validation, inventory check, payment, fulfillment status). It owns the orders table, the line items table, and the business rules. It calls the Payment service for charges and the Inventory service for stock checks – services that also encapsulate business logic, not just data access.

Premature Decomposition#

Splitting into microservices before understanding the domain. The team creates service boundaries on day one based on guesses about what the bounded contexts are, then discovers those boundaries are wrong six months later.

The Problem#

Wrong service boundaries are expensive to fix. Merging two services requires data migration, API consolidation, CI pipeline changes, and monitoring reconfiguration. Moving a capability from one service to another requires similar effort. In a monolith, moving code between modules is a refactor. In microservices, it is a migration project.

The Rule#

Start with a well-structured monolith (or a modular monolith). Extract services only when you have a clear reason: independent scaling, independent deployment cadence, different team ownership, or different technology requirements. The second system you extract is much better than the first because you understand the domain by then.

When to Merge Services Back#

Merging services is not a failure. It is a sign that the team learned the domain better and is correcting course.

Merge when:

  • Two services always deploy together and a change in one requires a change in the other.
  • A single developer or team owns both services and the operational overhead provides no benefit.
  • Inter-service communication is the primary source of bugs and latency.
  • The services share a database and splitting it is not practical.
  • A “service” has fewer than a few hundred lines of business logic.

How to merge:

  1. Pick the service that will absorb the other. Usually the one with more business logic.
  2. Copy the absorbed service’s code into the surviving service as a module.
  3. Replace inter-service HTTP/gRPC calls with in-process function calls.
  4. Migrate the absorbed service’s database tables into the surviving service’s database.
  5. Update routing (API gateway, service mesh) to point all traffic to the surviving service.
  6. Decommission the absorbed service’s infrastructure.

The goal of microservices is not to have the most services. It is to have the right services – each independently deployable, independently scalable, and owned by a team that can move fast without coordinating with everyone else.