Microservices patterns
Written by: Tom Spencer
Apr 10, 2023 — 7 min readMicroservices application: Customers, accounts and money transfers
The code for this article is based on microservices, Event Sourcing and CQRS. It is built using Spring Cloud, Spring Boot and the Eventuate platform. The application is used in hands-on labs that are part of a microservices services class that is taught by Chris Richardson.
Required Architectural quality attributes
- Devops is
testability, deployability, maintainability
- Autonomous teams is
modularity
- long-lived is
modularity, evolvability
Standard architecture is monolithic architecture:
The monolithic pattern is not necessarily an antipattern it can fulfil:
- testability
- deployability
- maintainability
- modularity
- evolvability
The problem is that successful applications can grow. The application can grow and the rate of change increases. All teams would contribute to the large code base. The application no longer fits in developers heads but can become a big ball of mud. The technology stack can become obsolete but rewrites are not feasible.
Chris Richardson mentions The Art of Scalability, Martin Abbot and Michael Fisher
Here we have three dimensions for the scale cube. X axis is multiple copies on load balancer. Z axis is split by categories on requests. Y axis is functional decomposition, breaking by function.
The microservice architecture is an architectural style that structures an application as a set of services. Each microservice is:
- highly maintainable and testable
- loosely coupled
- independently deployable
- organized around business capabilities
- owned by a small team
Top tip: Start with one service per team and only split service if it solves a problem. Monzo, for instance, has ten services per developer which is excessive.
The above diagram shows individual services and functional decomposition on the front end to query each service. The complexity can be a downside but there are some benefits:
- maintainability: small service means easier to understand and change
- modularity: service API is impermeable means enforces modularity
- evolvability: evolve each service's technology stack independently
- testability: small service mean easier and faster to test
- deployability: each service is independently deployable
This can increase scalability and fault tolerance. Interprocess communication and partial failure and distributed data can increase complexity. There are challenges for integration testing. Each service is its own application and this can increase deployment challenges. Identifying service boundaries is challenging - getting it wrong can lead to a distributed monolith anti-pattern. Refactoring to microservices can take a long time. We are now going to talk about the individual services. Traditional 3 tiered architecture doesn't reflect reality: Presentation, Business Logic and Persistence are not only single. We can use hexagonal architecture to describe the individual microservice architectures.
The reason for a service and team is to expose an API including: Commands that change data, Queries that extract data without modifying it. Often it is better to use Asynchronous messaging to reduce coupling between components. It is common for services to emit events to signal that the state of the business object has changed. The service may invoke other services and subscribe to events.
Types of coupling
Coupling is inevitable but must be loose. There are two types of coupling:
- runtime coupling - order service cannot respond to a synchronous request until another service responds
- Design time coupling - changes to a service can involve changing the client
Design time coupling can be difficult because it forces teams to coordinate work. Cross team collaboration slows down development. Decision making within the team is ten times faster than cross team collaboration. The service should be able to change without changing the API. We should avoid sharing database tables in microservices.
Run time coupling can occur when two services are using the same database blocking the other service from doing its work. Ideally we should use separate databases for each service.
Runtime coupling
The trouble with synchronous interprocess communications is that they can lead to reduced availability. Synchronous coupling can make the services less available. Method calls are fast but services syncrhonously calling each other can inhibit response times. Regarding asynchronout messaging Chris Richardson mentions Enterprise Integration Patterns, Gregor Hohpe:
The sender sends a message over a channel to a recipient.
- Abstraction of message broker capabilities:
- apache kafka topics
- JMS queues and topics
- channel types:
- point-to-point - deliver to one recipient
- publish-subscribe - deliver to all recipients
Sagas
https://microservices.io/patterns/data/saga.html
The Order Service returns immediately and the Customer Service confirms whether if the order is possible. There is more availability but this is an eventually consistent design. The response doesn't validate the outcome immediately.
Another option to reduce is CQRS: https://microservices.io/patterns/data/cqrs.html
The responsibility of knowing the credit is owned by the Order Service. The response can then tell immediately whether the request is accepted.
Testing
The goal of Microservices is to enable Devops which requires automated testing. The complexity of the architecture requires good automated testing.
Deployment pipeline - laptop to production
Deployment pipeline per service
Testing should be made at the service level.
Consumer-driven contract testing
Verify that a service (a.k.a provider) and its clients (a.k.a consumers) can communicate while testing them in isolation.
Contract testing example
This is an example of a contract test:
org.springframework.cloud.contract.spec.Contract.make {
request {
method 'GET'
url '/orders/99'
}
response {
status 200
headers {
header('Content-Type': 'application/json;charset=UTF-8')
}
body('''{"orderId": "99", "state": "APPROVAL_PENDING"}''')
}
}
The above is API definition by example. Wiremock simulates the Order Service and simulates the API. We use the contracts to test the controller. With the same contract testing document we ensure that the two components can communicate effectively. This can help minimize end-to-end tests.
Testing in production
- challenge
- end-to-end testing is brittle, slow and costly
- end-to-end tests are a simulation of production
- no matter how much we test issues will appear in production
- Therefore:
- separate deployment (running in production) from release (available to users)
- test deployed code before releasing
- automate for fast deployment, rollback and roll forward
We can use Canary deployment and route test traffic through the new version of services to ensure that the service works effectively.
Transactions and Queries in Microservice Architecture
Distributed data patterns are important for transtactions and queries with Microservices. We use the Saga pattern to enable transactions across services.
https://microservices.io/patterns/data/saga.html
It is not correct to do joins across services:
SELECT DISTINCT c.*
FROM CUSTOMER c, ORDER o
WHERE
c.id = o.ID
AND c.id = ?
AND o.PAID_DATE >= ?
Joins within services are fine but across services would lead to design-time coupling. There are two patterns for queries across services:
- API composition
- CQRS
CQRS is more powerful.
https://microservices.io/patterns/index.html#data-management
This is API composition:
It is simple but can be inefficient involving too many round trips on the network.
This is CQRS:
It is more flexible but more complex and eventually consistent. This is a helpful overview of the different patterns: "A query that spans services cannot simply join across service databases since that's design-time coupling. So use the API Composition or CQRS patterns.
A command that spans services cannot use traditional distributed transactions since that's tight runtime coupling. So use the Saga pattern."
Saga coordination
Choreography Based Sagas
- coordination logic is code that publishes events and event handlers
- when a saga participant participant updates a business object, it publishes domain events announcing what it has done
- Saga participants have event handlers that update business objects
Benefits and drawbacks of choreography
Benefits
- often simple especially when using event sourcing
- loose runtime coupling
Drawbacks
- Decentralized implementation - potentially difficult to understand
- Risk of excessive design time coupling - e.g. Customer Service must know about Order events that affect credit
- Cyclic dependencies - services listen to each other's events
https://github.com/eventuate-examples/eventuate-examples-java-customers-and-orders