Microservices patterns

Written by: Tom Spencer

Apr 10, 20237 min read

Microservices 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: image

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.

image

Chris Richardson mentions The Art of Scalability, Martin Abbot and Michael Fisher

image

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.

image

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.

image

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:

image

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

image

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

image

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.

image

Deployment pipeline - laptop to production

image

Deployment pipeline per service

image

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

image

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

image

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: image

It is simple but can be inefficient involving too many round trips on the network.

This is CQRS: image

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

image

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