Blog/10 Microservices Anti-Patterns That Will Ruin Your System
microservicesanti-patternsdistributed-systemsarchitecture

10 Microservices Anti-Patterns That Will Ruin Your System

March 15, 2024·13 min read·by Bishwambhar Sen
Tangled web of service connections representing a distributed monolith with no clear boundaries

Concept

Microservices architecture is a tool for organizational scaling, not a silver bullet for technical problems. The pattern earns its complexity cost only when teams need autonomous deployment, independent scaling, and technology heterogeneity across bounded domains. Every architectural decision that undermines those three properties destroys the value proposition while preserving the costs.

The ten anti-patterns below are structural, not cosmetic. Most are not visible in a PR review. They emerge from org dynamics, time pressure, and the natural human tendency to reuse rather than isolate. Recognizing them early — before they calcify into production topology — is the differentiating skill.

Anti-Pattern 1: The Distributed Monolith

A distributed monolith appears to be microservices: multiple deployable units, separate repositories, different teams. But every deployment requires coordination across all teams, because the services share either a release cycle, a shared database, or bidirectional runtime dependencies.

Diagnostic: Count how many teams need to be notified before one team can deploy to production. If the answer is greater than zero, you have coupling. If the answer is "all of them," you have a distributed monolith.

Root cause: Services were extracted by technical layer (the "services" team, the "data" team) rather than by business capability. Conway's Law is operating, not the inverse Conway maneuver.

Anti-Pattern 2: Shared Database (The Omniscient Schema)

Two or more services read and write to the same database schema. This is the most common — and most damaging — anti-pattern. It creates:

  • Schema coupling: One service cannot evolve its data model without coordinating with every other service that reads those tables.
  • Deployment coupling: A database migration blocks the deployment of all consumers.
  • Implicit contract leakage: The database becomes an undocumented API. Column names and table structures become contracts with no versioning.

The test: Can you replace a service's underlying database without any other team's knowledge or involvement? If not, it's shared.

Anti-Pattern 3: Chatty Services

A service that requires 10–20 network round-trips to a downstream service to complete a single user request. This multiplies latency (10 × 10ms = 100ms minimum, before variance), floods connection pools, and amplifies the blast radius of any downstream instability.

Root cause: Overly granular service design driven by OOP instincts ("one method per service call") rather than network-aware domain modeling. REST resources mapped 1:1 to entity CRUD operations, with no batch or projection capability.

Fix: Design APIs around coarse-grained use-case contracts, not fine-grained entity operations. Add bulk endpoints. Use GraphQL or BFF (Backend for Frontend) patterns to batch related queries.

Anti-Pattern 4: The Synchronous Dependency Chain

Service A calls B, which calls C, which calls D — all synchronously. The effective latency is the sum of all hops. One slow or unavailable service stalls the entire chain. The failure mode is a cascade: D is slow, C times out waiting for D, B times out waiting for C, A returns 503 to the user — and the user retries, multiplying the load on an already struggling system.

Fix: Break the chain at any point where the downstream response is not needed for the immediate user-facing response. The fix is event-driven or at minimum decoupled via a message queue, with a synchronous acknowledgment returned immediately.

Anti-Pattern 5: Missing Service Boundaries (Nanoservices)

The opposite of the distributed monolith: services that are too small. A service that represents a single function, a single DB table, or a single entity has no meaningful autonomy — it cannot make any domain decision without calling another service first.

Diagnostic: A service that cannot execute any business logic without first calling at least one other service is a nanoservice. It provides isolation overhead without isolation benefit.

Fix: Align service boundaries with bounded contexts from Domain-Driven Design. A bounded context is the minimum cohesive unit where a team can make domain decisions independently.

Anti-Pattern 6: Synchronous Saga Coordination

A saga that coordinates distributed state changes through a central orchestrator that makes synchronous, blocking calls to each participant. Under happy-path conditions, this looks fine. Under failure conditions — a participant is slow, a network partition occurs — the orchestrator blocks, holds locks or compensation state, and becomes a single point of coupling.

Anti-Pattern 7: No Idempotency on Message Consumers

A message consumer that processes the same event twice will corrupt state. At-least-once delivery is the operational default for every serious message broker. "At-least-once" means "sometimes exactly twice during partition healing, node failover, or re-delivery after consumer crash."

The failure mode: Order placed twice, payment charged twice, inventory decremented twice. All triggered by a single user action, caused by a re-delivered message.

Fix: Every message consumer must be idempotent. The idempotency key is the message ID. Use a processed-events table or a distributed deduplication cache with a TTL window longer than your maximum re-delivery interval.

Anti-Pattern 8: Health Checks That Lie

A service's health endpoint returns 200 OK while the service is incapable of serving traffic — because its downstream dependencies (database, cache, message broker) are unhealthy but the health check only pings the service process itself.

Consequence: The load balancer routes traffic to a "healthy" service that will fail every request. Incident detection is delayed; blast radius is maximized.

Fix: Implement readiness probes separately from liveness probes. Liveness: is the process alive? Readiness: is the service capable of handling traffic, including its downstream dependencies?

Anti-Pattern 9: Direct Service-to-Service Discovery with Hardcoded Addresses

Services communicate using hardcoded IP addresses or DNS names that embed environment-specific configuration. Deploying to a new environment, region, or cluster requires code changes, not configuration changes.

Fix: Service discovery through a service mesh, Kubernetes DNS, or a service registry (Consul). The service name is the stable identifier; the physical address is resolved at runtime.

Anti-Pattern 10: Logging Without Correlation IDs

Each service logs independently with no shared trace identifier. When a request fails, debugging requires manually correlating timestamps across 4–6 separate log streams — a 30-minute exercise for each incident, multiplied by every on-call rotation.

Fix: W3C traceparent header propagation (see the Observability post). Every log line should include the trace ID, span ID, and service name as structured fields.

Constraints

The Autonomy Paradox

The fundamental constraint of microservices is that every inter-service dependency reduces autonomy. This is not solvable — it is managed. The architectural objective is to minimize necessary dependencies and make their contracts explicit, versioned, and stable.

The maximum autonomy baseline: a service can be deployed, scaled, and rolled back by its owning team without scheduling coordination with any other team. If this isn't true for at least 80% of your services, your microservices migration has not yet achieved its stated objective.

The Two-Pizza Rule Is Not Enough

Conway's Law operates on communication structure, not team size. Ten two-pizza teams all reporting to the same VP of Engineering, attending the same architecture review board, and working in the same monorepo will produce a system that is functionally monolithic regardless of how the code is packaged.

Trade-offs

Microservices vs. Modular Monolith

The modular monolith deserves a serious seat at the table. Sam Newman (author of Building Microservices) explicitly advises starting with a monolith and extracting services only when a specific scaling driver emerges.

Property Microservices Modular Monolith
Independent deployability Yes No
Operational complexity High Low
Local development experience Poor (Docker Compose sprawl) Excellent
Inter-module latency Network (ms) In-process (µs)
Team autonomy at scale High Medium
Debugging complexity High (requires tracing) Low (single process)

The right answer depends on team size, deployment frequency, and whether you have genuinely independent scaling requirements across domains. A team of 8 engineers should very seriously consider the modular monolith before accepting microservices complexity.

Code

The following shows a correct idempotent message consumer implementation in .NET — the pattern every microservice that consumes events must implement:

// IdempotentOrderEventConsumer.cs
public class OrderPlacedEventConsumer : IMessageConsumer<OrderPlacedEvent>
{
    private readonly IProcessedEventRepository _processedEvents;
    private readonly IOrderRepository _orders;
    private readonly ILogger<OrderPlacedEventConsumer> _logger;

    public OrderPlacedEventConsumer(
        IProcessedEventRepository processedEvents,
        IOrderRepository orders,
        ILogger<OrderPlacedEventConsumer> logger)
    {
        _processedEvents = processedEvents;
        _orders = orders;
        _logger = logger;
    }

    public async Task ConsumeAsync(
        OrderPlacedEvent message,
        MessageContext context,
        CancellationToken cancellationToken = default)
    {
        // Idempotency check — the message ID is the deduplication key
        if (await _processedEvents.AlreadyProcessedAsync(context.MessageId, cancellationToken))
        {
            _logger.LogInformation(
                "Duplicate message {MessageId} for order {OrderId} — skipping",
                context.MessageId, message.OrderId);
            return; // Ack the message to remove it from the queue — do NOT reprocess
        }

        // Process within a transaction that atomically marks as processed
        await using var transaction = await _orders.BeginTransactionAsync(cancellationToken);

        try
        {
            var order = Order.Reconstitute(message);
            await _orders.SaveAsync(order, cancellationToken);

            // Mark processed WITHIN the same transaction — prevents partial processing
            await _processedEvents.MarkProcessedAsync(
                context.MessageId,
                processedAt: DateTimeOffset.UtcNow,
                expiresAt: DateTimeOffset.UtcNow.AddDays(7), // TTL for cleanup
                cancellationToken);

            await transaction.CommitAsync(cancellationToken);

            _logger.LogInformation(
                "Order {OrderId} processed from message {MessageId}",
                message.OrderId, context.MessageId);
        }
        catch (Exception ex)
        {
            await transaction.RollbackAsync(cancellationToken);
            _logger.LogError(ex, "Failed to process order event {MessageId}", context.MessageId);
            throw; // Re-throw — broker will redeliver
        }
    }
}

The second example demonstrates an anti-chatty API: a composite query endpoint that eliminates 5 chatty calls with one coarse-grained response, using a read-model pattern:

// OrderSummaryQueryHandler.cs
// Anti-pattern: 5 separate service calls to render one order page
// Fix: one composite query handler backed by a dedicated read model

public class OrderSummaryQuery
{
    public required Guid OrderId { get; init; }
    public required Guid CustomerId { get; init; }
}

public record OrderSummaryReadModel
{
    public Guid OrderId { get; init; }
    public string CustomerName { get; init; } = string.Empty;
    public string CustomerEmail { get; init; } = string.Empty;
    public IReadOnlyList<OrderLineItem> Items { get; init; } = [];
    public decimal TotalAmount { get; init; }
    public string Currency { get; init; } = string.Empty;
    public OrderStatus Status { get; init; }
    public string? TrackingNumber { get; init; }
    public DateTimeOffset PlacedAt { get; init; }
    public DateTimeOffset? EstimatedDelivery { get; init; }
}

// This read model is updated by event handlers — not assembled at query time
// Eliminates: GetOrder() + GetCustomer() + GetItems() + GetTracking() + GetDelivery()
public class OrderSummaryQueryHandler
{
    private readonly IOrderSummaryReadModelRepository _readModel;

    public OrderSummaryQueryHandler(IOrderSummaryReadModelRepository readModel)
    {
        _readModel = readModel;
    }

    public async Task<OrderSummaryReadModel?> HandleAsync(
        OrderSummaryQuery query,
        CancellationToken cancellationToken = default)
    {
        // Single database read — denormalized, no cross-service calls at query time
        return await _readModel.FindByIdAsync(query.OrderId, cancellationToken);
    }
}

Further Reading

External references: