Microservices Architecture: When to Split and When Not to

Microservices are not a technology — they’re an organizational strategy. The right reason to split a monolith is team autonomy and independent deployment, not technical elegance. This article covers when splitting makes sense and how to do it without creating a distributed monolith.

What Microservices Actually Solve

Microservices address two problems:

1. Independent deployment: Team A can deploy the Order Service without coordinating with Team B’s Payment Service. No shared deployment pipeline, no release freeze, no “all-hands” deploy windows.

2. Independent scaling: The Search Service needs 50 instances during peak. The Admin Service needs 2. In a monolith, you scale everything.

If neither problem applies to you — small team, manageable deployment cadence, no extreme scaling difference between components — microservices add cost without benefit.

The Cost of Microservices

Every service boundary you add creates:

  • Network latency: In-process call (microseconds) → HTTP/gRPC call (milliseconds)
  • Partial failure: Service A can be up while Service B is down — you need circuit breakers, retries, timeouts
  • Distributed transactions: Changing data across two services requires sagas or two-phase commit
  • Operational overhead: Each service needs its own CI/CD, monitoring, scaling config, health checks
  • Integration testing: Testing the interaction between 10 services is orders of magnitude harder than testing one codebase
  • Data consistency: Enforcing referential integrity across service databases is a solved problem in monoliths, unsolved in microservices

This is not a reason to never use microservices. It’s a reason to understand that microservices are a trade that pays complexity for autonomy.

When to Split

Split when:

  • Different teams own different capabilities — and they’re blocked by each other’s deployments
  • Different scaling requirements — one component needs 100x more resources than others
  • Different technology requirements — the ML pipeline needs Python, the API needs Java
  • Different reliability requirements — a non-critical reporting service shouldn’t be able to take down the payment flow
  • Compliance isolation — PCI DSS requires payment card data in a separate, audited environment

Don’t split when:

  • You’re a small team (< 30 engineers)
  • You’re building the first version of something (the boundaries will be wrong — you don’t know the domain well enough yet)
  • You want to “future-proof” — premature splitting creates complexity without delivering autonomy
  • The services would need to communicate synchronously for every user request (tight coupling)

Domain-Driven Design: Finding Service Boundaries

The right service boundaries align with business capabilities, not technical layers:

❌ Wrong (technical layers)           ✓ Right (business capabilities)
───────────────────────────           ─────────────────────────────────
  UI Service                          Order Service
  API Gateway                         Payment Service
  Business Logic Service              Inventory Service
  Data Service                        Customer Service
                                      Notification Service

Use Bounded Contexts from Domain-Driven Design. Each bounded context has its own ubiquitous language, its own model of the domain, and its own data store.

Order Service:       "Order" = items, quantities, shipping address
Customer Service:    "Customer" = profile, preferences, address book
Payment Service:     "Order" = payment reference, amount, status

The same word means different things in different contexts — that’s a signal of a real boundary.

Inter-Service Communication

Synchronous: REST or gRPC

OrderController → [HTTP] → InventoryService → [HTTP] → WarehouseService

Use when: you need the response before you can proceed (user is waiting).

Problems: cascading failures (if Inventory is slow, Order is slow), tight coupling at the network level.

Asynchronous: Kafka Events

OrderService → [Kafka: order-events] → InventoryService (async)
                                     → NotificationService (async)

Use when: the response can be eventual (fire and forget, or eventual consistency is acceptable).

Problems: more complex error handling, eventual consistency requires careful thought.

Rule of thumb

If a user action triggers the call and the user needs the result now → REST. If the action triggers downstream work that can happen later → Kafka.

Service Communication Patterns

API Gateway

All external traffic goes through a single entry point:

Internet → API Gateway → Order Service
                       → Customer Service
                       → Payment Service

The gateway handles: rate limiting, authentication, SSL termination, routing, request aggregation.

Backend for Frontend (BFF)

Mobile app has different data needs than the web app. Instead of one generic API gateway, create one per client:

Mobile App → Mobile BFF → multiple internal services
Web App    → Web BFF    → multiple internal services

Each BFF aggregates exactly the data each client needs — no over-fetching.

Service Mesh

For complex deployments, a service mesh (Istio, Linkerd) handles: mTLS, circuit breaking, retries, observability at the infrastructure level — not in application code.

Data Isolation: One Database Per Service

Each service owns its data. No other service touches that service’s tables:

Order Service     → orders_db (PostgreSQL)
Payment Service   → payments_db (PostgreSQL)
Inventory Service → inventory_db (PostgreSQL)
Search Service    → elasticsearch cluster

This is the hardest part of microservices. You can no longer do:

-- This cross-service JOIN is now impossible
SELECT o.id, c.name, p.status
FROM orders o
JOIN customers c ON o.customer_id = c.id
JOIN payments p ON o.payment_id = p.id

Instead, each service denormalizes the data it needs at event time, or you query multiple services and aggregate in the BFF.

The Distributed Monolith Anti-Pattern

The worst outcome: services that are deployed independently but must be deployed together because they’re tightly coupled:

// Don't do this — Order Service directly calls Payment DB
@Service
public class OrderService {
    @Autowired PaymentRepository paymentRepository;  // wrong database!
}

// Don't do this — synchronous calls that must all succeed together
public Order createOrder(CreateOrderRequest request) {
    inventoryService.reserve(request.items());   // if this fails...
    paymentService.charge(request.payment());    // ...this never runs
    notificationService.send(order);             // ...or this
    return orderRepository.save(order);
}

Signs you have a distributed monolith:

  • Services must deploy in a specific order
  • Integration tests require most services to be running
  • A change in Service A consistently requires changes in Service B
  • Services share database tables

Strangler Fig: Migrating from Monolith to Microservices

Don’t rewrite everything at once. Extract one service at a time:

Phase 1: Monolith handles everything
Phase 2: Extract Notification Service (low risk, few dependencies)
Phase 3: Extract Payment Service (highest business value, clear boundary)
Phase 4: Extract Inventory Service
Phase 5: Remaining Order/Customer code stays as a "core monolith"

The API Gateway routes new endpoints to new services; old endpoints still hit the monolith. The monolith shrinks over time — “strangled” by the surrounding services.

What You’ve Learned

  • Microservices solve team autonomy and independent scaling — not technical complexity
  • The cost: network latency, partial failure, distributed transactions, operational overhead
  • Split along business capabilities (bounded contexts), not technical layers
  • One database per service — no cross-service SQL joins
  • Use REST for synchronous user-facing calls; Kafka for async background work
  • The strangler fig pattern extracts services incrementally — don’t big-bang rewrite
  • A distributed monolith (tightly coupled services) is the worst of both worlds — avoid it

Next: Article 47 — Service Discovery with Eureka — let services find each other without hardcoded addresses.