Microsserviços

DDD and Microservices - From Business to Architecture

MicroService architecture has gained popularity in recent years, but it involves much more than creating and maintaining a solution made up of a variety of "micro" services.

First, when does a distributed approach become more appropriate than a monolithic approach? What criteria to use to isolate and distribute services? What are the impacts of adopting a distributed strategy over monolithic strategy?

To answer the questions above, we will understand how and why to apply the mindset of Domain Driven Design in refactoring a monolithic solution into a distributed solution, shedding some common market biases and being aware of the challenges this approach will bring to solution development, testing, delivery, and maintenance.

Motivation

Domain Driven Design, or DDD, is a set of principles and practices for bringing the technical solution closer to the business it serves, in a way that it can be expanded rather than limited as business understanding grows. Basically, technical decisions are tied to business needs, not merely “technology by technology” or “patterns by patterns”, such as development focused on industry standards or fads that are not directly related to the business problem.

The microservices architecture is an example of a concept that may end up being adopted as a market trend, without the team being aware of the challenges or even the advantages it can bring to the solution.

In two previous articles, Demystifying the DDD and Distilling a domain with DDD, I explain some of the principles of DDD and how they can affect business and software strategies.

In this article I will show how the mindset DDD can be used to model microservices in a business-aligned manner, or even to question whether microservices is the most appropriate choice for the scenario at hand.

DDD and Microservices - The Best of Both Worlds

When we combine DDD principles with the microservices architecture, we enable decentralized management according to the business context. A given context may require specific needs, both in development (language, paradigm, platform) and infra (bare metal vs virtualization, number of instances). When the model is mature, it allows for fast deliveries, resilient environment and elasticity.

However, failure to implement the basics can turn what would be a microservice solution into a monolith composed of tightly coupled services - in practice what was once a centralized point of failure and a single service to version, deliver and sustain, turns into tens or hundreds.

According to Conway's Law [1] - “Any organization that designs a system (…) will produce a design whose structure is a copy of the organization's communication structure”, that is, the implementation of the solution tends to reflect the business structure, which It is often not best suited to take advantage of the flexibility that the microservice architecture offers. If this happens, one can imagine that the organization of microservices will reflect the organization of teams, for example, front end, business rules, infrastructure, etc.

Conway's Law vs DDD
Conway's Law vs DDD

In contrast, by applying DDD principles, services can be organized according to the context they serve - for example, related services are published to the same cluster to reduce the delay between operations.

To exemplify the root of some of the challenges of microservice architecture, we will demonstrate in the following topics a scenario in which one of the modules of a monolithic solution has been isolated, and will analyze the direct impacts of this refactoring from the DDD perspective.

Scenario

The application is a point-for-product exchange portal, similar to an e-commerce. The authenticated user browses a Web Portal, selects products according to their point balance, and then requests the exchange. The Shopping Cart system transforms the transaction and returns the order protocol to the user while triggering the logistics flow. The user will receive within 24 hours an email with the order confirmation and delivery date.

Context Map

Mapa de Contexto
Context Map

In this scenario, the Web Portal and the Shopping Cart system were developed internally, with integrations for a market authentication system (Azure Active Directory, for example) and a custom external logistics system.

Points for products transaction

The main flow of context can be depicted as follows:

Fluxo principal
Main flow

If we highlighted the Shopping Cart submission transaction already created, we would have a flow similar to the one shown below:

Fluxo de submissão do carrinho de compras
Shopping Cart Submission Flow

The internal logistics service consists of an adapter that translates the cart object to the object to be sent to the external logistics system. The Shopping Cart, Customer, and Inventory I / O repositories are within the Shopping Cart work unit, and the flow is contained in a database transaction. If a problem occurs, such as a product suddenly unavailable or communication with the Logistics System fails, the transaction will be rolled back. Otherwise it will be confirmed.

Building blocks

For illustration purposes, imagine that the Shopping Cart system was organized with the following components and building blocks:

Building Blocks do Serviço de Carrinho de Compras
Shopping Cart Service Building Blocks

Advantages in the monolithic approach

It is very common to hear associations between the monolithic approach and legacy projects full of technical debt, but it is incorrect to suppose that one thing is a consequence of another. A new solution developed in microservices may also contain critical technical outputs and architectural limitations.

A monolithic application is not necessarily bad. It can be well implemented and efficiently solve the proposed problem if the solution can really be considered as a unit, including reflecting DDD principles, if any.

Comparing a monolithic layered application consistently developed with an equivalent solution distributed in microservices, we can see advantages in several aspects:

  • Development and debug: Preparing a development environment is simpler. Integration failures between layers result in compilation errors, which can be quickly noticed and corrected. The debug process allows you to “navigate” between the different layers involved in each transaction, making it possible to perform end-to-end testing without major difficulties.
  • Integration Tests: Usually the application connects to a few databases, so it is relatively simple to work with disposable masses of data;
  • Data Integrity: Centralized database gives you greater control over data integrity. Clearing traces of an unsuccessful transaction is almost immediate;
  • Execution Speed: Running in a single process ensures greater speed. Delay between the different execution steps of a transaction is rarely a problem;
  • Continuous Construction and Delivery: The operational effort to maintain a single build / deploy repository and wake is low.

When the need to isolate one of the modules arises

At some point in the project life cycle, the team may come to the conclusion that a particular module needs to be isolated and have a life cycle of its own. Some of the reasons may be:

  • Differentiated demand compared to the solution as a whole - there are considerably more consumers for specific features of that module rather than the complete solution;
  • Dependence on distinct hardware or software features to make separate management more suitable, for example running on a physical and non-virtual server for higher performance;
  • A new product will be built from a particular module by business needs;

If there is a concrete reason for separating one or more modules, it is important to understand the impact of refactoring and the concerns that arise when adopting a distributed strategy.

Refactoring the solution

To illustrate, imagine that the inventory module will be transformed into an isolated service.

Isolamento do Serviço de Estoque
Inventory Service Isolation

The direct impacts could be:

  • Isolation of shared code so that both services (Shopping Cart and Inventory) consume functionality common to both centrally;
  • Removal or adaptation of inventory code in the Shopping Cart Service;
  • Separation of stock database;

Shared Core

It will be necessary to decide how systems will handle shared code - typically infrastructure or the famous "utils" - generic functionality that has been abstracted to be reused to speed development.

The first option is to duplicate these codes in the new service that will include the inventory module. The second, used in this example, is to isolate shared code in a library and distribute it across both services. The third is to provide functionality via service if it makes sense.

Duplicating the code may sound like a antipatternbut it's interesting when functionality can grow at different speeds and needs among consumers.

If the code is really generic, you can distribute it as a component. In this case it will have its delivery mat to some suitable component repository (Nexus, Package Management, etc.). Sound version management is critical (for example, {Major}. {Minor}. {Patch}, incrementing Major when a contract breaks, incrementing Minor for new functionality and fixes, and associating the Patch number with the ID of the artifact to ensure uniqueness and tracking). In addition, to ensure compatibility, when publishing a new version, it is necessary to keep the previous ones available as long as there are consumers.

And there are scenarios where shared code should be distributed via service, not as a component. If the functionality contained in the component needs to be 100% up-to-date for all consumers in real time, as is the case with business rules, modifying the component will require updating and redeploying each of its consumers, generating an operational effort and a risk. The best alternative would be to make these features available via service to ensure that consumers always have access to the latest version available.

Inventory Service

After isolating the infra module, the inventory module and its database are also isolated. As shown below, the Inventory Service contains only the components relevant to the inventory subdomain.

Building Blocks do Serviço de Estoque
Inventory Service Building Blocks

In the Shopping Cart Service part of the inventory code has been removed, but some components remain - for example the inventory service, which instead of the actual implementation will be a communication adapter with the new inventory service. The Product entity still exists, but only with content relevant to that context.

Building Blocks do Serviço de Carrinho de Compras refatorado
Refurbished Shopping Cart Service Building Blocks

Impact on Points Redemption Transaction

The points redemption transaction depends on the products being reserved in inventory to be completed. The user waits for the transaction to complete with his protocol number and expects it to happen in a few moments.

What happens when communication with the Inventory service fails, either by bug or connection problems (timeout, network permissions, etc.)?

Fluxo principal refatorado
Refactored Main Flow

In this scenario, some proper Retry policy needs to be implemented to communicate with the Inventory service. A malformed or declined request does not require Retry, but a communication failure timeout may allow a certain number of retries while maintaining the circuit break principles [2].

If even with Retry the communication is not successful, the logical rollback should be performed, probably with a SAGA strategy [3] since the databases are distributed.

Impact on development and testing

Imagine that now Shopping Cart Service and Inventory Service are maintained by different teams. Compatibility failures are no longer noticed at compile time! You will need to use some contract testing strategy to ensure communication compatibility.

Integration testing depends not only on a unique and disposable data mass for the Shopping Cart Service but now for the Inventory Service as well. The Inventory Service endpoint used for the Shopping Cart Service integration test needs to be accessible exclusively to the Shopping Cart Service, as a parallel change in data mass may compromise the reliability of the integration test.

The latency between Shopping Cart Service and Inventory Service communication is another point of attention that did not exist in the monolithic approach. When transactions are executed in a single process, latency is usually not taken into account. However, when distributing steps of the same transaction in isolated services, latency will often only be perceived in the productive environment, not in development time, even due to the difficulty in performing integrated tests.

In addition to latency, certain risks that are not obvious in the development environment need to be anticipated, such as communication failure due to permission or unavailability.

Other points of attention that arise when migrating from a monolithic to a microservice solution are described in the paper “The 8 Fallacies of Distributed Computing” [4].

Final considerations

mindset Domain Driven Design can be applied to both monolithic and microservice solutions, affecting design decisions (how objects and events will be modeled), software (technology, platforms and dependencies) and hardware (what resources will be used and how the solution will be distributed).

Monolithic solutions efficiently address most market scenarios.

Using internal components / libraries brings versioning, distribution and compatibility challenges. If consumers are required to have access to the latest version, make the functionality available via service and not via component. If consumers have different needs, replicate the code on each consumer for greater flexibility of change rather than centering on one component.

Distributing functionality across services entails challenges in management, development, testing, and operations. Do not distribute resources that could be contained in the same service unless there is a concrete justification.

Justifications and distribution strategies will yield better results if they are aligned with the business rather than corporate standards (Conway's Law) or patterns and badly placed fads.

References

Published by

Grazi Bonizi

I lead the .Net Architecture track at The Developers Conference, share code on GitHub, write on Lambda3 Medium and Blog, and participate in Meetups and PodCasts typically on DevOps, Azure, .Net, Docker / Kubernetes, and DDD

Leave a Reply