Designing Business Logic in a Microservice Architecture
From Microservices Patterns by Chris Richardson
The heart of an enterprise application is the business logic that implements the business rules. In a microservice architecture the business logic is spread over multiple services. Some external invocations of the business logic are handled by a single service. Other, more complex requests, are handled by multiple services and sagas are used to enforce data consistency. In this article, I describe how to implement a service’s business logic.
The raison d’etre for a service is to handle requests from its clients. Some clients are external to the application, such as other applications or the user. Other clients are services, like saga orchestrators. Inbound requests are handled by an adapter such as a web controller or messaging gateway, which invokes the business logic. The business logic typically updates a database, possibly invokes other services, and returns a response to the request.
Developing complex business logic is always challenging. What’s more, the microservice architecture presents some distinctive challenges. A microservice-based application often uses sagas to maintain consistency across multiple services. Furthermore, as you’ll learn below, in a microservice architecture business logic often needs to generate events when data changes. In this article, you’ll learn how to structure business logic as a collection of domain-driven design (DDD) aggregates that emit events. Let’s first look at the different ways of organizing business logic.
Business logic organization patterns
The core of a service is its business logic. But as figure 1 shows, a service also consists of one or more adapters. It has inbound adapters, which handle requests from clients and invoke the business logic. It typically has outbound adapters, which enable the business logic to invoke other services and applications.
This service consists of the business logic and the following adapters:
- REST API adapter, which exposes an HTTP API.
- Inbound message gateway, which consumes messages from a message channel.
- Database adapter, which accesses the database
- Outbound message adapter, publishes messages to a message broker
Sitting at the core of the service is the business logic, which is typically the most complex part of the service and it’s invoked by the inbound adapters. The business logic invokes the outbound adapters to access the database and publish messages.
When developing business logic, you should consciously organize your business logic in the way which is the most appropriate for your application. After all, I’m sure you’ve experienced the frustration of having to maintain someone else’s badly structured code. Most enterprise applications are written in an object-oriented language such as Java and consist of classes and methods. Using an object-oriented language doesn’t guarantee that the business logic has an object-oriented design. The key decision you must make when developing business logic is whether to use an object-oriented approach or a procedural approach. The two main patterns for organizing business logic are the procedural Transaction Script pattern, and the object-oriented Domain Model pattern.
Transaction script pattern
Although I’m a strong advocate of the object-oriented approach, there are some situations where it’s overkill, such as when you’re developing simple business logic. In such a situation, a better approach is to write procedural code and use what Martin Fowler calls the Transaction Script pattern [Fowler, 2002]. Rather than doing any object-oriented design, you write a method, which is called a transaction script, to handle each request from the presentation tier. As figure 2 shows, an important characteristic of this approach is that the classes that implement behavior are separate from those that store state.
When using the Transaction Script pattern, the scripts are usually located in service classes, which in this example is the
OrderService class. A service class has one method for each request/system operation. The method implements the business logic for that request. The data objects, which in this example is the
Order class, are pure data with little or no behavior.
This style of design is highly procedural, and relies on few of the capabilities of object-oriented programming (OOP) languages. This is what you’d create if you were writing the application in C or another non-OOP language. Nevertheless, you shouldn’t be ashamed to use a procedural design when it’s appropriate. This approach works well for simple business logic. The drawback is that this tends to be a poor way to implement complex business logic.
Domain Model pattern
The simplicity of the procedural approach can be seductive. You can write code without having to carefully consider how to organize the classes. The problem is when your business logic becomes complex, and your code becomes a nightmare to maintain. In fact, in the same way that a monolithic application has a habit of continually growing, transaction scripts have the same problem. Consequently, unless you’re writing an extremely simple application you should resist the temptation to write procedural code, and instead apply the Domain Model pattern and develop an object-oriented design.
In an object-oriented design, the business logic consists of an object model, which is a network of relatively small classes. These classes typically correspond directly to concepts from the problem domain. In such a design some classes have either state or behavior but many contain both, which is the hallmark of a well-designed class. Figure 3 shows an example of the Domain Model pattern.
As with the Transaction Script pattern, an
OrderService class has a method for each request/system operation. When using the Domain Model pattern, the service methods are usually extremely simple. This is because a service method almost always delegates to persistent domain objects, which contain the bulk of the business logic. A service method might, for example, load a domain object from the database and invoke one of its methods. In this example, the
Order class has both state and behavior. Moreover, its state is private and can only be accessed indirectly via its methods.
Using an object-oriented design has a number of benefits. First, the design is easier to understand and maintain, instead of consisting of one big class that does everything, it consists of a number of small classes that each have a small number of responsibilities. In addition, classes such as Account, BankingTransaction, and OverdraftPolicy closely mirror the real world, which makes their role in the design easier to understand. Second, our object-oriented design is easier to test: each class can and should be tested independently. Finally, an object-oriented design is easier to extend because it can use well-known design patterns, such as the Strategy pattern and the Template Method pattern [Gang of Four], that define ways of extending a component without modifying the code.
The Domain Model pattern works well, Numerous problems come up with this approach, like with a microservice architecture. To address these problems you need to use a refinement of OOD known as Domain Driven design.
Domain-Driven design is a refinement of OOD and an approach for developing complex business logic. DDD subdomains are a useful concept when decomposing an application into services. When using DDD, each service has its own domain model, which avoids the problems of a single, application-wide domain model. Subdomains and the associated concept of Bounded Context are two of the strategic DDD patterns.
DDD also has some tactical patterns that are building blocks for domain models. Each pattern is a role that a class plays in a domain model and defines the characteristics of the class. The building blocks which have been widely adopted by developers include:
- Entity — an object with a persistent identity. Two entities whose attributes have the same values are still different objects. In a Java EE application, classes which are persisted using JPA
@Entityare usually DDD entities.
- Value object — an object which is a collection of values. Two value objects whose attributes have the same values can be used interchangeably. An example of a value object is a
Moneyclass, which consists of a currency and an amount.
- Factory — an object or method that implements object creation logic which is too complex to be done directly by a constructor. A factory might be implemented as a static method of a class.
- Repository — an object that provides access to persistent entities and encapsulates the mechanism for accessing the database.
- Service — an object that implements business logic which doesn’t belong in an entity or a value object.
These building blocks are used by many developers. Some are supported by frameworks such as JPA and the Spring framework. One more building block exists, which has been generally ignored by most (myself included!), excluding DDD purists: aggregates. It turns out that aggregates are an extremely useful concept when developing microservices. But that’s a topic for another article.
If you want to learn more about the book, check it out on liveBook here.
About the author:
Chris Richardson is a Java Champion, a JavaOne rock star, author of Manning’s POJOs in Action, and creator of the original CloudFoundry.com.
Originally published at freecontent.manning.com.