Master Hexagonal and DDD Architecture: A Practical Guide to Designing Scalable Microservices

In the modern world of software development, microservices architecture has become a standard because of its advantages in scalability and maintenance. A microservice is a small, autonomous unit of functionality that interacts with other services through well-defined APIs. In this guide, we're going to explore configuring a microservice with multiple integrations. This example includes essential services such as Apache Kafka, MongoDB, ActiveMQ, Artemis, and HTTP.

In addition, this microservice follows the hexagonal architecture pattern with the domain-directed design (DDD) approach. This means that business logic is at the center of the design, and external dependencies are at the edges, allowing for better modularization and unit testing of business logic.

Introduction to Hexagonal Architecture

The hexagonal architecture, also known as port and adapter architecture, was introduced by Alistair Cockburn with the goal of creating a software design that is easier to maintain and extend. This architecture seeks to separate the system's business logic from external dependencies, such as databases, user interfaces, and other services.


src/main/java/com/kranio/
  ├── application
  │   ├── mappers
  │   │   └── UserMapper.java
  │   ├── services
  │   │   └── UserService.java
  ├── domain
  │   ├── classes
  │   │   └── User.java
  │   ├── repositories
  │   │   └── IUserRepository.java
  ├── infrastructure
  │   ├── amq/repositories
  │   │   └── AMQProducerRepository.java
  │   ├── mongodb/repositories
  │   │   └── MongoRepository.java
  ├── presenters
  │   ├── http
  │   │   └── UserController.java
  │   ├── kafka
  │   │   └── KafkaConsumer.java
 

Explanation of the Components

1. Application

• mappers

• UserMapper.java: Class for mapping data between different layers of the application.



package com.kranio.application.mappers;
	
	import jakarta.enterprise.context.ApplicationScoped;
	import com.kranio.domain.classes.User;
	import jakarta.xml.bind.JAXBContext;
	import jakarta.xml.bind.JAXBException;
	import jakarta.xml.bind.Unmarshaller;
	
	import java.io.StringReader;
	
	@ApplicationScoped
	public class UserMapper {
	
	  public User convertStringToUser(String xml) throws JAXBException {
	    JAXBContext jaxbContext = JAXBContext.newInstance(User.class);
	    Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
	    StringReader reader = new StringReader(xml);
	    return (User) unmarshaller.unmarshal(reader);
	  }
	}



• services

• UserService.java: Implementation of the business logic of the user service.



package com.kranio.application.services;
	
	import jakarta.enterprise.context.ApplicationScoped;
	import jakarta.inject.Inject;
	import com.kranio.domain.repositories.IUserRepository;
	import com.kranio.application.mappers.UserMapper;
	import com.kranio.domain.classes.User;
	
	@ApplicationScoped
	public class UserService {
	
	  @Inject
	  private IUserRepository userRepository;
	
	  @Inject
	  private UserMapper userMapper;
	
	  public void saveAndSendMessage(String message) {
	    try {
	      User user = userMapper.convertStringToUser(message);
	      String response = userRepository.save(user);
	      userRepository.sendMessage(response);
	    } catch (Exception e) {
	      System.err.println("Error saving User: " + e.getMessage());
	    }
	  }
	
	  public void saveAndSendMessage(User user) {
	    try {
	      String response = userRepository.save(user);
	      userRepository.sendMessage(response);
	    } catch (Exception e) {
	      System.err.println("Error saving User: " + e.getMessage());
	    }
	  }
	}
  



2. Domain

• classes

• User.java: Class that represents the user model.



package com.kranio.domain.classes;
	
	import io.quarkus.mongodb.panache.PanacheMongoEntity;
	import io.quarkus.mongodb.panache.PanacheMongoEntityBase;
	import lombok.Data;
	
	public class User extends PanacheMongoEntity {
	    public String name;
	    public String email;
	}



• repositories

• IUserRepository.java: Interface that defines user persistence operations.



package com.kranio.domain.repositories;
	
	import com.kranio.domain.classes.User;
	
	public interface IUserRepository {
	  public void sendMessage(String message);
	
	  public String save(User user);
	}
  



3. Infrastructure

• amq/repositories

• AMQProducerRepository.java: Implementation of the output port to interact with ActiveMQ.



package com.kranio.infrastructure.amq.repositories;
	
	import jakarta.enterprise.context.ApplicationScoped;
	import jakarta.inject.Inject;
	import jakarta.jms.ConnectionFactory;
	import jakarta.jms.JMSContext;
	import jakarta.jms.JMSRuntimeException;
	import jakarta.jms.Session;
	import com.kranio.domain.classes.User;
	import com.kranio.domain.repositories.IUserRepository;
	import com.kranio.infrastructure.mongodb.repositories.MongoRepository;
	
	@ApplicationScoped
	public class AMQProducerRepository implements IUserRepository {
	
	  @Inject
	  private ConnectionFactory connectionFactory;
	
	  @Inject
	  private MongoRepository mongoRepository;
	
	  public void sendMessage(String message) {
	    try (JMSContext context = connectionFactory.createContext(Session.AUTO_ACKNOWLEDGE)) {
	      context.createProducer().send(context.createQueue("TestQueue"), message);
	      System.out.println("Message sent: " + message);
	    } catch (JMSRuntimeException ex) {
	      System.err.println("Error sending message: " + ex.getMessage());
	    }
	  }
	
	  public String save(User user) {
	    mongoRepository.persist(user);
	    return "User saved";
	  }
	}




• mongodb/repositories

• MongoRepository.java: Implementation of the output port to interact with MongoDB.



package com.kranio.infrastructure.mongodb.repositories;
	
	import io.quarkus.mongodb.panache.PanacheMongoRepository;
	
	import jakarta.enterprise.context.ApplicationScoped;
	import com.kranio.domain.classes.User;
	
	@ApplicationScoped
	public class MongoRepository implements PanacheMongoRepository *User* {
	}



4. Presenters

• http

• UserController.java: Controller that handles HTTP requests and converts them into calls to business logic through the user service.



package com.kranio.presenters.http;
	
	import jakarta.inject.Inject;
	import jakarta.ws.rs.POST;
	import jakarta.ws.rs.Path;
	import jakarta.ws.rs.core.Response;
	
	import com.kranio.application.services.UserService;
	import com.kranio.domain.classes.User;
	
	@Path("/user")
	public class UserController {
	
	  @Inject
	  private UserService userService;
	
	  @POST
	  public Response create(User newUser) {
	    userService.saveAndSendMessage(newUser);
	    return Response.status(Response.Status.CREATED).entity(newUser).build();
	  }
	}




• Kafka

• KafkaConsumer.java: Kafka message consumer that interacts with business logic.



package com.kranio.presenters.kafka;
	
	import org.eclipse.microprofile.reactive.messaging.Incoming;
	import io.vertx.mutiny.ext.auth.User;
	import jakarta.inject.Inject;
	import jakarta.inject.Singleton;
	import com.kranio.application.services.UserService;
	
	@Singleton
	public class KafkaConsumer {
	
	  @Inject
	  private UserService userService;
	
	  @Incoming("consumer")
	  public void consume(String message) {
	    System.out.println("Consumiendo mensaje: " + message);
	    userService.saveAndSendMessage(message);
	  }
	}




This project structure based on the hexagonal architecture allows a clear separation of responsibilities, where the core of the application (domain) remains independent of the technologies and frameworks used in the adapters. This facilitates maintenance, scalability and unit testing, ensuring that business logic is kept clean and easily adaptable to future changes.

Benefits and Weaknesses of Hexagonal Architecture

Benefits

1. Separation of Concerns: Facilitates maintenance by separating business logic from technical dependencies.

2. Flexibility and Extensibility: Allows you to add new functionalities or change technologies without affecting the central logic.

3. Unit Tests: Facilitates the creation of unit tests by allowing the use of mocks or stubs for external dependencies.

4. Technological Independence: It makes it easier to change technologies (databases, messaging systems, etc.) without modifying business logic.

Weaknesses

1. Initial Complexity: Requires greater planning and definition of ports and adapters from the start.

2. Abstraction Overload: It can introduce unnecessary abstraction overload into small projects.

3. Learning Curve: It may be more difficult to understand and apply correctly for developers without experience in this architecture.

4. Performance: The additional layer of abstraction may introduce a slight decrease in performance, although it is generally negligible compared to the benefits.

Domain-Directed Design (DDD)

Domain-Directed Design (DDD) is a software development approach that places a strong emphasis on modeling the software's core business domain. Introduced by Eric Evans in his book “Domain-Driven Design: Tackling Complexity in the Heart of Software”, DDD offers a set of principles and patterns for creating software systems that deeply reflect the business domain and the needs of users.

Key DDD Concepts

1. Entities: Objects that have a distinctive identity that spans time and different states. For example, a user with a unique ID.

2. Value Objects: Objects that are completely defined by their attributes. They have no identity of their own. For example, an address.

3. Aggregates: A group of entities and value objects that are treated as a unit. They have a root entity that controls access.

4. Repositories: They facilitate access to aggregates. They act as a collection in memory of the aggregates.

5. Domain Services: Transactions that do not belong to any particular entity or object of value but are part of the domain.

6. Modules: Group together related concepts to organize the code.

7. Factories: Responsible for the creation of complex or aggregated objects.

  

DDD Benefits

1. Clear Business Model: DDD helps create a clear and shared business model between developers and domain experts.

2. More Understandable Code: The code becomes more understandable because it directly reflects the terms and processes of the business domain.

3. Reducing Complexity: By focusing on the core of the domain and its rules, DDD helps manage the complexity inherent in software systems.

4. Improved Communication: Improves communication between developers and domain experts using a common Ubiquitous Language.

5. Flexibility and Maintainability: Clear separation of responsibilities and modularization facilitate system extension and maintenance.

DDD Weaknesses

1. Steep Learning Curve: Requires a good understanding of DDD concepts and can be difficult for inexperienced teams to adopt.

2. Initial Overhead: The modeling process can be intensive and take longer in the early stages of development.

3. Complexity in Small Projects: You can introduce unnecessary complexity into small or simple projects.

4. Need for Constant Collaboration: Requires ongoing collaboration between developers and domain experts, which can be a challenge in some contexts.

5. Implementation Difficulty: Correctly identifying the boundaries of the context and the division of the domain can be challenging and may require several iterations.

And that's what the microservice looks like that will be attached for you to take a look at, clone it and play around a bit. Read the README.md to see how it runs and all the considerations to take into account.

https://github.com/eljoesb/ddd-kranio-blog

Ready to transform your application architecture with modern and efficient approaches?

At Kranio, we have a team of experts in Hexagonal Architecture and Domain-Directed Design (DDD) who will help you implement scalable and maintainable microservice solutions. Contact us and discover how we can promote the technological evolution of your company.

Jonathan Villavicencio

September 16, 2024