跪拜 Guibai
← Back to the summary

Microservices Done Right: Splitting, Communicating, and Governing Without Creating a Distributed Monolith

1. Introduction: Why Microservices

Monolithic applications are the most efficient choice in the early stages of a business: one repository, one build, one deployment, low communication overhead for the team, and a clear debugging path. But when business scale, team size, and iteration frequency all grow at once, the costs of the monolith start to show:

Microservices alleviate these problems by splitting along business boundaries, deploying independently, and evolving independently. But one thing needs to be clear: microservices are not a free lunch. They trade "code complexity" for "distributed systems complexity." This article follows the order of "decomposition → communication → governance → data consistency" to lay out a workable design methodology.

The diagram below summarizes the overall thread of this article; the subsequent chapters follow this order:

flowchart LR
  A["Service decomposition"] --> B["Service communication"]
  B --> C["Service governance"]
  C --> D["Data consistency"]
  A -.-> A1["Single responsibility / independent deployment / decentralization"]
  B -.-> B1["Sync vs async / gateway and BFF"]
  C -.-> C1["Registration discovery / config / circuit breaking / observability"]
  D -.-> D1["Eventual consistency / idempotency / compensation"]

2. Core Principles of Service Decomposition

2.1 Single Responsibility: Split by Business Capability, Not by Technical Layer

The most common mistake is splitting by technical layer (Controller service, Service service, DAO service). This causes every business change to require releases across multiple services, amplifying coupling instead of reducing it.

The right approach is to split by business capability, referring to the Bounded Context in Domain-Driven Design (DDD):

Splitting approach Example Evaluation
By technical layer User Controller service / User DAO service ❌ One requirement touches multiple services
By business capability User service / Order service / Inventory service ✅ Change cohesion
By resource type Image service / Video service ⚠️ Needs case-by-case business judgment

A simple standard to judge whether a service has a "single responsibility": do all the reasons for changing this service belong to the same business domain? If two features are always modified for different reasons, they should be split; if they are always modified together, they should be merged.

The evolution path from monolith to microservices can be understood like this:

flowchart TD
  M["Monolithic application"] --> S1["Identify bounded contexts by business capability"]
  S1 --> S2["Split out 3~8 core domain services"]
  S2 --> S3["Independent deployment and independent data"]
  S3 --> C{"Still need to release together?"}
  C -->|"Yes"| W["Distributed monolith: boundary was wrong, must re-divide"]
  C -->|"No"| OK["Boundaries are clear, refine as needed"]

2.2 Independent Deployment: Splitting Only Counts When You Can Deliver Independently

A service is truly "independent" only if it has the following capabilities:

If two services still must "ship together" after the split, the decomposition only added operational cost without bringing any benefit.

2.3 Decentralization: Devolving Data, Governance, and Decision-Making

Decentralization covers three levels:

  1. Data decentralization: each service owns its data and exposes it via APIs or events, rather than letting other services connect directly to its database.
  2. Governance decentralization: teams choose their own technology stacks and implementation details; the platform team only provides standard infrastructure (registry center, config center, observability).
  3. Decision decentralization: the team that owns a service decides its internal implementation details; only contracts are agreed across services.

Note: decentralization does not mean "no standards." Cross-service contracts like interface protocols, error code conventions, log formats, and trace instrumentation must be unified, or governance costs will rise sharply.

2.4 Splitting Granularity: From Coarse to Fine, Avoid Over-Splitting

There is no standard answer for splitting granularity, but there are some pieces of experience:

3. Service Communication Patterns

3.1 Synchronous vs Asynchronous

Dimension Synchronous (HTTP/gRPC) Asynchronous (MQ/events)
Coupling Time-coupled; caller waits Time-decoupled; producer and consumer do not block each other
Real-time Strong; immediate result Weak; eventual consistency
Failure impact Downstream failures propagate upstream Downstream failures can be buffered and retried
Complexity Clear chain, easy to debug Need to handle idempotency, ordering, duplicate consumption
Use cases Queries, strong-consistency validation Notifications, state transitions, cross-domain events

Practical recommendations:

The choice between sync and async can be decided by the following path:

flowchart TD
  Q["One inter-service call"] --> R{"Does the caller need an immediate result?"}
  R -->|"Yes"| S["Synchronous HTTP/gRPC"]
  R -->|"No"| T{"Is it part of the core transaction path?"}
  T -->|"Yes"| U["Sync + timeout + circuit breaker"]
  T -->|"No"| V["Async MQ/event"]
  S --> W["Must set timeout and retry backoff"]
  V --> X["Consumer guarantees idempotency and ordering"]

3.2 API Gateway and BFF

An API gateway sits at the system's entry point and handles cross-cutting concerns:

BFF (Backend for Frontend) is an aggregation layer after the gateway that tailors APIs for specific frontends (Web, App, mini-program):

Web frontend ──┐
App frontend ──┼──> API gateway ──> BFF(Web) ──┐
Mini-program ──┘                BFF(App) ──┼──> User service / Order service / Product service
                                        └──> ...

The value of BFF:

The cost is an extra layer of calls and operational overhead. Small teams or few client types do not need to force a BFF in.

Let's replace the above textual diagram with a flowchart to make the request path clearer:

flowchart LR
  W["Web frontend"] --> G["API gateway"]
  A["App frontend"] --> G
  M["Mini-program"] --> G
  G --> BW["BFF(Web)"]
  G --> BA["BFF(App)"]
  BW --> U["User service"]
  BW --> O["Order service"]
  BA --> U
  BA --> P["Product service"]

3.3 Protocol Selection

4. Service Governance

4.1 Service Registration and Discovery

After a service instance starts, it registers its address with a registry center. Callers address the service by name rather than by IP:

Service provider ──register──> Registry center (Nacos / Consul / Eureka)
                          │
Caller ──subscribe/pull────┘
   │
   └──> Load balancing selects an instance ──> make the call

Key design points:

The complete flow of a service call in the registration/discovery system:

sequenceDiagram
  participant P as "Service provider"
  participant R as "Registry center"
  participant C as "Caller"
  participant L as "Load balancer"
  P->>R: "Register instance address at startup"
  R-->>P: "Heartbeat and health check"
  C->>R: "Subscribe to service list"
  R-->>C: "Push instance list (local cache)"
  C->>L: "Request by service name"
  L->>P: "Select healthy instance and call"
  P-->>C: "Return result"
  P->>R: "Remove before shutdown"

4.2 Configuration Management

Config centers (Nacos Config, Apollo, Spring Cloud Config) solve three types of problems:

  1. Environment isolation: dev/test/prod configs are separated to avoid accidental misuse.
  2. Dynamic updates: rate-limit thresholds, feature switches, timeouts can be adjusted without restarting the service.
  3. Change audit: who changed what config and when can be traced and rolled back.

Practical points:

4.3 Circuit Breaking and Degradation

A circuit breaker is a state machine:

stateDiagram-v2
    [*] --> Closed
    Closed --> Open: "Error rate exceeds threshold"
    Open --> HalfOpen: "Wait window ends"
    HalfOpen --> Closed: "Probe request succeeds"
    HalfOpen --> Open: "Probe request fails"

Degradation strategies usually include:

Alongside this are timeout control and retry policy: retries must use backoff and a max count, and should only be applied to idempotent operations; otherwise they amplify failures.

4.4 Observability

The premise of governance is "being able to see." Three pillars are all required:

5. Distributed Transactions and Eventual Consistency

5.1 Why Distributed Transactions Are Needed

After splitting services, a single business operation can span multiple service databases. For example, "place an order" needs to write to the order database, deduct inventory, and deduct balance. Local transactions cannot cover cross-database operations, so consistency problems arise.

5.2 Comparison of Common Approaches

Approach Consistency Performance Invasiveness Use cases
2PC/XA Strong Low High Few internal strong-consistency scenarios
TCC Eventual Medium High Core transactions like money and inventory
Saga Eventual High Medium Long-running, multi-step business
Local message table Eventual High Low Async notification scenarios
Transactional message (RocketMQ) Eventual High Low Scenarios deeply integrated with MQ

5.3 Practice: Local Message Table + Scheduled Compensation

This is the lowest-cost and most commonly used eventual-consistency approach:

1. Local transaction: business data + message record are written in the same local transaction
2. Async delivery: a scheduled task scans unsent messages and delivers them to MQ
3. Consumption: downstream consumes and guarantees idempotency
4. Status write-back: after successful delivery, update the message status to sent
5. Failure compensation: messages exceeding the max retry count go to a dead-letter queue for manual intervention

Key points:

5.4 Three Baseline Rules for Consistency Design

  1. Do not sacrifice availability for strong consistency: most business scenarios can tolerate second-level eventual consistency.
  2. Idempotency is the premise of consistency: any write operation that may be retried must be idempotent.
  3. Reconciliation as a backstop: no matter how good compensation is, something can still be missed; core money-related business must have a scheduled reconciliation mechanism.

6. Summary

The core of microservices architecture design is not "how finely you split," but clear boundaries, stable contracts, and solid governance:

Architecture evolution has no end. Start with the smallest feasible split, and let monitoring data drive the next optimization, rather than designing a "perfect architecture" all at once.