How to get your code ready for microservices

Component/Model Separation

The first step is to split the entire codebase into components and a domain model. Components contain most of the business logic, and the domain model contains data, state, and domain model logic. It helps if you separate them at the architectural level - components depend on the model, but not the other way around.

What are components and models and what are their properties?

Component

  • is a Spring Bean - we can use injection, AOP and other Spring stuff.
  • is a singleton - there is only one instance in the Spring context.
  • is stateless - doesn't hold any state / any value.
  • Is configurable - an exception in statelessness is the configuration possibility that can be injected by Spring or optionally changed at runtime.
  • Good terminology should be used - we use Spring, so we use Spring terms: Controller, Service, Repository as suffixes (see below).

Model

  • Not a Spring Bean - no spring injection possible.
  • is a POJO class - with data fields and associated domain model logic.
  • are immutable - all fields are final, collections are immutable, and constructors are used for creation.
  • Cloneable - when you need to modify an object. Use the .withXXX methods.
  • Has a valid creation - it should not be created in an invalid state. All conditions (NotNulls etc.) should be checked upon creation in the constructor.
  • Contains domain model logic only - only logically related to model data, no external state related logic. Usually formatting, parsing, validation, simple calculations, etc.

Domain Model Logic Restrictions

Avoiding external state is perhaps the most typical limitation of domain model logic. Now you can look at the last picture and see that the model doesn't have any dependencies on any other artifacts. So the logic there can't call any other services, and certainly can't store anything in the database (or itself). It's best not to pass external state indirectly (for example, by passing parameter values ​​from domain model logic into the model). Every parameter passed there increases memory consumption, and these models can exist in large numbers. Also, parameter values ​​and domain data are somewhat mixed.

Reasons to use component/model separation

  • Simple schema - easy to define, easy to understand, easy to maintain.
  • Can be thread-safe - used with other methods like concurrent collections or locks.
  • Multi-tenant environment - Allows multiple tenants to request the same instance.
  • Save some memory - Fewer instances in memory with shorter lifetimes.
  • Immutable classes - better suited for the garbage collector.
  • Improved error proofing - thanks to statelessness and immutability.
  • Standardized Terminology - Allows for faster understanding of code.

Controller/Service/Storage Layer

The second step is to separate the components into controller, service and repository layers. We should also separate them at build level in this way: Controller -> Service -> Repository.

Decoupling code internally into layers is very useful for code evolution. We divide them into three layers:

  1. Controller - It contains all frontend communication like REST API. It allows you to replace the controller layer if you want to use other technologies (such as GraphQL) or batch processing, while the business logic in the service remains as it is.
  2. Services - This contains all business logic. Unless you change the input/output contract, the changes should not affect any other layers.
  3. Repository - This contains all backend communication like SQL database etc. Allows you to replace the repository layer with another persistence technology.

For communication between layers we can use Data Transfer Objects (DTO). To simplify, we can use a domain model that belongs mainly to the service layer. We can also create additional DTOs for Controller and Repository layers if we want to have a clean separation (and strictly follow the Single Responsibility Principle).

As we said, these layers should be separated at the build level (for example, via Maven artifacts). Make sure no unwanted dependencies leak into the wrong layer (for example, REST API leaks into the service layer, or even a database library leaks into the controller layer). Also, make sure that controllers and repositories have nothing to do with business logic and vice versa. Use this rule of thumb: always ask yourself if a controller/repository can be replaced as-is with a new controller/repository, and doesn't require reimplementing anything from business logic to the new controller/repository.

Now things get a little more complicated, so packages should be named. Best practice seems to be that the name of the artifact should be created from part of the package name to be able to easily find the artifact from the Stacktrace (eg name of the artifact: manta-controller from eu.manta.controller.user). We can avoid the package name prefix because it is obvious in most cases, and the suffix (after the layer name) because it contains the artifact.

Reasons to use a controller/service/repository layer

  • Allows for replaceable controllers and repositories.
  • Clear controllers/repositories from business code and vice versa.
  • Separation of code responsibilities.

Port/Adapter Architecture

The third step is to create contracts between the layers - ports - and reverse the direction of dependencies between the service layer and the storage layer. At the build level, the dependencies look like this: Controller -> InputPort <- Service -> OutputPort <- Repository. This architectural pattern is also known as Hexagonal Architecture and was created by Alistar Cockburn.

This is the next step in layered architecture. It solves the obvious problem that the Service layer depends on the Repository, so the Repository is irreplaceable. Also, we have two new artifacts here: an input port and an output port. They contain interfaces that define contracts between layers. They can also contain DTOs if we think they are useful or if we want to adhere to the Single Responsibility Principle.

port

  • Explicitly define input contracts - via interfaces and optionally DTOs.
  • From the point of view of the Service layer, it is an input.
  • They can be called from the controller layer.
  • The xxxPort interface should be implemented by the Service layer.
  • Easier testing - we can avoid using Controller/Repository or use mock instead.

adapter

  • Clearly define output contracts.
  • From the perspective of the Service layer, its output.
  • They can be called from the service layer.
  • xxxAdapter needs Repository layer to implement.

Guess you like

Origin blog.csdn.net/wouderw/article/details/128089513