Decouple by domain-driven design
Microservices should be designed around business capabilities, not horizontal layers such as data access or messaging. Microservices should also have loose coupling and high functional cohesion. Microservices are loosely coupled if you can change one service without requiring other services to be updated at the same time. A microservice is cohesive if it has a single, well-defined purpose, such as managing user accounts or processing payment.
data synchronization, transactional integrity, joins, and latency
Practical Design
Reference tables
In monolithic applications, it’s common for modules to access required data from a different module through an SQL join to the other module’s table.
In microservices applications, there are the following options for isolating database objects.
Share data through an API
A service uses an API call to get data from another service.
This implementation has obvious performance issues due to additional network and database calls. However, sharing data through an API works well when data size is limited. Also, if the called service is returning data that has a well-known rate of change, you can implement a local TTL cache on the caller to reduce network requests to the called service.
Replicate data
Data from a service is replicated in a dependent service database.
Another way to share data between two separate microservices is to replicate data in the dependent service database. The data replication is read-only and can be rebuilt any time. This pattern enables the service to be more cohesive.
This implementation lets a service get data without repeated calls to the dependent service.
The replicated data is eventually consistent, but there can be lag in replicating data, so there is a risk of serving stale data.
To build data replication, you can use techniques like materialized views, change data capture (CDC), and event notifications.
Static data as configuration
Static data, such as country codes and supported currencies, are slow to change. You can inject such static data as a configuration in a microservice. Modern microservices and cloud frameworks provide features to manage such configuration data using configuration servers, key-value stores, and vaults. You can include these features declaratively.
Shared mutable data
Monolithic applications have a common pattern known as shared mutable state. In a shared mutable state configuration, multiple modules use a single table.
The solution is to develop a separate microservice to manage the shared mutable database tables.
Distributed transactions
After you isolate the service from the monolith, a local transaction in the original monolithic system might get distributed between multiple services. A transaction that spans multiple services is considered a distributed transaction. In the monolithic application, the database system ensures that the transactions are atomic. To handle transactions between various services in a microservice-based system, you need to create a global transaction coordinator. The transaction coordinator handles rollback, compensating actions, and other transactions.
The following patterns are commonly used to handle distributed transactions:
- Two-phase commit protocol (2PC)
- Saga
We recommend Saga for long-lived transactions. In a microservices-based application, you expect interservice calls and communication with third-party systems. Therefore, it’s best to design for eventual consistency: retry for recoverable errors and expose compensating events that eventually amend non-recoverable errors.
There are various ways to implement a Saga—for example, you can use task and workflow engines such as Apache Airflow, Apache Camel, or Conductor. You can also write your own event handlers using systems based on Kafka, RabbitMQ, or ActiveMQ.
Data consistency
Distributed transactions introduce the challenge of maintaining data consistency across services. All updates must be done atomically.
Consider a multistep transaction in a microservices-based architecture. If any one service transaction fails, data must be reconciled by rolling back steps that have succeeded across the other services. Otherwise, the global view of the application’s data is inconsistent between services.
It can be challenging to determine when a step that implements eventual consistency has failed.
Interservice communication
In a monolithic application, components (or application modules) invoke each other directly through function calls. In contrast, a microservices‑based application consists of multiple services that interact with each other over the network.
When you design interservices communication, first think about how services are expected to interact with each other. Service interactions can be one of the following:
- One‑to‑one interaction: each client request is processed by exactly one service.
- One‑to‑many interactions: each request is processed by multiple services.
Also consider whether the interaction is synchronous or asynchronous:
- Synchronous: the client expects a timely response from the service and it might block while it waits.
- Asynchronous: the client doesn’t block while waiting for a response. The response, if any, isn’t necessarily sent immediately.
| One-to-one | One-to-many | |
|---|---|---|
| Synchronous | Request and response: send a request to a service and wait for a response. | — |
| Asynchronous | Notification: send a request to a service, but no reply is expected or sent. | Publish and subscribe: the client publishes a notification message, and zero or more interested services consume the message. |
| Asynchronous | Request and asynchronous response: send a request to a service, which replies asynchronously. The client doesn’t block. | Publish and asynchronous responses: the client publishes a request, and waits for responses from interested services. |
Implement interservices communication
communication mechanisms:
-
synchronous request-response‑based communication mechanisms
such as HTTP‑based REST, gRPC, or Thrift
-
asynchronous, message‑based communication mechanisms
such as AMQP or STOMP
message formats:
-
text‑based formats
such as JSON or XML
-
binary format
such as Avro or Protocol Buffers
Asynchronous communication between services can be implemented using messaging or event-driven communication:
- Messaging: When you implement messaging, you remove the need for services to call each other directly. Instead, all services know of a message broker, and they push messages to that broker. The message broker saves these messages in a message queue. Other services can subscribe to the messages that they care about.
- Event-based communication: When you implement event-driven processing, communication between services takes place through events that individual services produce. Individual services write their events to a message broker. Services can listen to the events of interest. This pattern keeps services loosely coupled because the events don’t include payload.
Advantages and disadvantages
advantages:
- Loose coupling
- Failure isolation
- Responsiveness
- Flow control
disadvantages:
- Latency
- Overhead in development and testing
- Throughput
- Complicates error handling
Reference
- https://en.wikipedia.org/wiki/Microservices
- https://en.wikipedia.org/wiki/Service_discovery
- https://en.wikipedia.org/wiki/SOA_governance
- https://grpc.io/blog/grpc-load-balancing/
- https://github.com/grpc/grpc/blob/master/doc/load-balancing.md
- https://www.nginx.com/blog/introduction-to-microservices/
- https://github.com/gliderlabs/registrator
- https://github.com/micro/go-micro
- https://github.com/micro/examples
- https://github.com/apache/incubator-brpc/tree/master/example