Microservices Architecture: Modern Patterns and Anti-Patterns

Published 12 min read
microservicesservice designdistributed systemsAPI contracts

Microservices architecture is a design approach where an application is composed of small, independently deployable services, each owning a specific business domain. When implemented well, microservices enable teams to ship independently, scale individual components, and choose the right technology for each problem. When implemented poorly, they create a distributed monolith that is slower and harder to debug than the monolith it replaced.

When Microservices Make Sense (and When They Do Not)

Microservices add operational complexity. Before adopting them, verify that the benefits outweigh the costs for your team and product stage.

FactorMicroservices FitMonolith Fit

Team size15+ engineers, multiple teams1-10 engineers, single team
Deployment frequencyMultiple deploys per day per serviceWeekly or bi-weekly releases
Scaling requirementsDifferent components scale independentlyUniform scaling sufficient
Technology diversityDifferent languages/runtimes neededSingle technology stack
Domain complexityClear, separable business domainsTightly coupled domain logic
Organizational structureConway's Law alignment neededSingle team owns everything

The most common mistake is adopting microservices too early, before the team has clear domain boundaries or operational maturity. Start with a well-structured monolith, then extract services when you have evidence that independent deployment or independent scaling is needed.

Pattern 1: Domain-Driven Service Boundaries

Service boundaries should align with business domains, not technical layers. A "user service" that owns user registration, authentication, profile management, and user preferences is better than separate services for each of those functions.

How to identify service boundaries:

  • Map your domain into bounded contexts using Domain-Driven Design (DDD)
  • Identify which contexts change independently and are owned by different teams
  • Look for natural seams where inter-context communication is infrequent
  • Validate boundaries by checking: can this service be deployed independently without coordinating with other services?
  • Good service boundary indicators:

    • The service owns its data completely (no shared database tables)
    • Changes to this service rarely require changes to other services
    • A single team can own and operate the service end-to-end
    • The service has a clear, stable API contract

    Bad service boundary indicators:

    • Two services must always be deployed together
    • A change requires updating 3+ services simultaneously
    • Services make synchronous calls to each other in tight loops
    • Data is duplicated across services with manual sync processes

    For platform options that support independent service deployment, see Services Overview.

    Pattern 2: Contract-First API Design

    Define API contracts before writing implementation code. This enables parallel development across teams, automated compatibility testing, and clear documentation.

    <h1>OpenAPI contract-first example</h1>
    

    openapi: 3.0.3

    info:

    title: Order Service API

    version: 1.0.0

    paths:

    /orders:

    post:

    summary: Create a new order

    requestBody:

    required: true

    content:

    application/json:

    schema:

    $ref: "#/components/schemas/CreateOrderRequest"

    responses:

    "201":

    description: Order created

    content:

    application/json:

    schema:

    $ref: "#/components/schemas/Order"

    "400":

    description: Invalid request

    Contract-first workflow:

  • Define the API contract in OpenAPI or protobuf
  • Review the contract with consuming teams before implementation
  • Generate server stubs and client libraries from the contract
  • Implement the server to fulfill the contract
  • Run contract tests in CI to catch breaking changes
  • API Versioning Strategies

    StrategyProsConsBest For

    URL versioning (/v1/orders)Simple, explicitURL changes on every versionExternal APIs
    Header versioning (Accept: v2)Clean URLsLess discoverableInternal APIs
    Additive changes onlyNo versioning overheadCannot remove fieldsMost services

    For most internal microservices, additive-only changes with deprecation notices eliminate the need for explicit versioning. Only introduce version numbers when breaking changes are unavoidable.

    Pattern 3: Asynchronous Messaging for Decoupling

    Synchronous HTTP calls between services create tight coupling and cascading failures. When Service A calls Service B synchronously, an outage in B causes an outage in A. Asynchronous messaging breaks this dependency.

    Use async messaging for:

    • Event notification: "An order was placed" (other services react independently)
    • Command distribution: "Process this payment" (producer does not wait for result)
    • Data synchronization: "Customer address changed" (consuming services update their copies)

    <h1>Event publishing example</h1>
    

    async def create_order(order_data):

    # Save to local database

    order = await db.orders.create(order_data)

    # Publish event (async, non-blocking)

    await event_bus.publish("order.created", {

    "order_id": order.id,

    "customer_id": order.customer_id,

    "total": order.total,

    "items": order.items

    })

    return order

    Message broker comparison:

    BrokerOrderingPersistenceThroughputBest For

    Apache KafkaPartition-orderedDurable logVery HighEvent streaming, audit logs
    RabbitMQQueue-orderedOptionalHighTask distribution, RPC
    NATS JetStreamStream-orderedOptionalVery HighCloud-native, lightweight
    Redis StreamsStream-orderedConfigurableHighSimple event bus, low latency

    Pattern 4: Saga Pattern for Distributed Transactions

    In a microservices architecture, you cannot use traditional database transactions across services. The Saga pattern coordinates multi-service workflows through a sequence of local transactions with compensating actions for rollback.

    Example: Order placement saga

  • Order Service: Create order (status: pending)
  • Payment Service: Reserve payment
  • Inventory Service: Reserve stock
  • Order Service: Confirm order (status: confirmed)
  • If step 3 fails:

  • Payment Service: Release payment reservation (compensating action)
  • Order Service: Cancel order (compensating action)
  • Saga Implementation Approaches

    ApproachCoordinationComplexityBest For

    ChoreographyEvents between servicesLowerSimple, few-step sagas
    OrchestrationCentral saga coordinatorHigherComplex, multi-step sagas

    Choreography works well for 2-3 step sagas. Beyond that, orchestration provides better visibility into saga state and easier debugging.

    Pattern 5: Platform Standards for Operational Consistency

    Without platform standards, each microservice team reinvents logging, metrics, health checks, deployment, and configuration management. This creates operational chaos at scale.

    Define and enforce standards for:

    • Health check endpoints: Standard /health and /ready paths with consistent response format
    • Logging format: JSON structured logging with required fields (timestamp, level, service, trace_id)
    • Metrics exposition: Prometheus-compatible /metrics endpoint with RED metrics
    • Configuration management: Environment variables for secrets, config files for everything else
    • Deployment manifests: Standard Kubernetes resource templates with required labels and annotations
    • Error response format: Consistent error codes, messages, and correlation IDs across all services

    // Standard error response format
    

    {

    "error": {

    "code": "ORDER_NOT_FOUND",

    "message": "Order with ID 12345 does not exist",

    "trace_id": "abc123def456",

    "timestamp": "2025-03-03T10:30:00Z"

    }

    }

    For managed deployment standards and service lifecycle, explore Cloud Computing.

    Anti-Pattern 1: Shared Database

    Multiple services reading from and writing to the same database tables is the most common microservices anti-pattern. It creates hidden coupling: a schema change in one service breaks another.

    Symptoms:

    • Services cannot be deployed independently because they share schema migrations
    • Performance issues in one service affect others sharing the database
    • Teams cannot change their data model without coordinating with other teams

    Fix: Each service owns its database. If services need each other's data, they request it through APIs or subscribe to domain events.

    Anti-Pattern 2: Distributed Monolith

    A distributed monolith has the complexity of microservices with none of the benefits. Services are separately deployed but tightly coupled through synchronous call chains, shared libraries, or coordinated releases.

    Symptoms:

    • Deploying one service requires deploying 2-3 others simultaneously
    • A change in one service requires updating shared libraries used by other services
    • Services cannot function when any of their dependencies are down

    Fix: Introduce async messaging, eliminate shared libraries (duplicate code is acceptable), and redesign boundaries around true domain independence.

    Anti-Pattern 3: Nano-Services

    Over-decomposing into too many tiny services creates excessive network overhead, complex debugging, and deployment coordination problems. A service that is only 50-100 lines of code is probably too small.

    Symptoms:

    • A single user request hits 10+ services
    • Latency is dominated by network hops, not business logic
    • Teams spend more time on inter-service communication than on features

    Fix: Merge related nano-services back into cohesive domain services. A service should own a meaningful business capability, not a single function.

    Testing Microservices

    The testing pyramid shifts significantly in a microservices architecture. Integration tests and contract tests become more important than unit tests.

    Test TypeScopeSpeedConfidencePriority

    Unit testsSingle function/classFastLow for integrationMedium
    Contract testsAPI compatibilityFastHigh for contractsHigh
    Integration testsService + dependenciesMediumHigh for behaviorHigh
    End-to-end testsFull user journeySlowHighestLow (few, critical paths)

    Contract testing with tools like Pact ensures that service API changes do not break consuming services without requiring full end-to-end test suites.

    Review Documentation for testing framework guides and implementation patterns.

    Migration Strategy: Monolith to Microservices

    The strangler fig pattern is the safest approach for migrating from a monolith to microservices:

  • Identify a bounded context to extract (choose one with clear boundaries and high change frequency)
  • Build the new service alongside the monolith
  • Route traffic to the new service for the extracted functionality
  • Remove the old code from the monolith once the new service is stable
  • Repeat for the next bounded context
  • Critical rules for migration:

    • Never do a big-bang rewrite. Extract one service at a time.
    • Keep the monolith working throughout the migration. It is your fallback.
    • Extract the highest-value service first to prove the pattern.
    • Accept that migration will take months or years, not weeks.

    Compare infrastructure options for your migration at Pricing. For architecture review and planning, Contact us.

    Frequently Asked Questions

    Q: How many microservices should we have?

    There is no magic number. A good heuristic is one service per team (5-8 engineers). If a single team owns more than 3-4 services, they are probably too granular. If a service requires more than one team to develop and deploy, it is probably too large.

    Q: Should microservices share code through libraries?

    Minimize shared libraries. Shared client libraries for API communication are acceptable. Shared business logic libraries create coupling and coordinated release requirements. When in doubt, duplicate code rather than share it.

    Q: How do we handle data consistency across microservices?

    Accept eventual consistency as the default. Use the Saga pattern for workflows that span multiple services. Design services to handle out-of-order events and duplicate messages (idempotency). Only use synchronous calls when immediate consistency is truly required.

    Q: What is the right communication protocol between services?

    Use HTTP/REST or gRPC for synchronous request-response. Use message brokers (Kafka, RabbitMQ, NATS) for asynchronous events. gRPC offers better performance and type safety for internal service communication. REST is simpler and better for external APIs.

    Q: How do we debug issues that span multiple microservices?

    Distributed tracing is essential. Propagate trace IDs through all service calls and include them in log entries. Tools like Jaeger or Tempo visualize the full request path. Without tracing, debugging cross-service issues is extremely difficult.