
Master Hexagonal Architecture and DDD: A Practical Guide to Designing Scalable Microservices
In the modern world of software development, microservices architecture has become a standard due to 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 will explore the setup of a microservice with multiple integrations. This example includes essential services such as Apache Kafka, MongoDB, ActiveMQ Artemis, and HTTP.
Additionally, this microservice follows the hexagonal architecture pattern with a domain-driven design (DDD) approach. This means that the business logic is at the center of the design, and external dependencies are at the edges, allowing for better modularization and unit testing of the business logic.
Introduction to Hexagonal Architecture
Hexagonal architecture, also known as ports and adapters architecture, was introduced by Alistair Cockburn with the goal of creating 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 to map 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 for 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 outbound 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 outbound 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 the 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 the 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("Consuming message: " + message);
  userService.saveAndSendMessage(message);
 }
}
This project structure based on 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 the business logic remains 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 adding new features or changing technologies without affecting the core logic.
3. Unit Testing: Facilitates the creation of unit tests by allowing the use of mocks or stubs for external dependencies.
4. Technological Independence: Makes it easier to change technologies (databases, messaging systems, etc.) without modifying the business logic.
Weaknesses
1. Initial Complexity: Requires more planning and definition of ports and adapters from the start.
2. Abstraction Overhead: Can introduce unnecessary abstraction overhead in small projects.
3. Learning Curve: Can be harder to understand and apply correctly for developers without experience in this architecture.
4. Performance: The additional abstraction layer may introduce a slight decrease in performance, although it is generally insignificant compared to the benefits.
‍
Domain-Driven Design (DDD)
Domain-Driven Design (DDD) is a software development approach that places strong emphasis on modeling the core business domain of the software. 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 to create software systems that deeply reflect the business domain and user needs.
Key Concepts of DDD
1. Entities: Objects that have a distinctive identity that persists over time and different states. For example, a user with a unique ID.
2. Value Objects: Objects that are fully defined by their attributes. They have no identity of their own. For example, an address.
3. Aggregates: A group of entities and value objects treated as a unit. They have a root entity that controls access.
4. Repositories: Facilitate access to aggregates. Act as an in-memory collection of aggregates.
5. Domain Services: Operations that do not belong to any particular entity or value object but are part of the domain.
6. Modules: Group related concepts to organize the code.
7. Factories: Responsible for creating complex objects or aggregates.
Benefits of DDD
1. Clear Business Model: DDD helps create a clear business model shared 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. Complexity Reduction: By focusing on the core domain and its rules, DDD helps manage the inherent complexity of software systems.
4. Improved Communication: Enhances communication between developers and domain experts through a common ubiquitous language.
5. Flexibility and Maintainability: Clear separation of responsibilities and modularization facilitate system extension and maintenance.
‍
Weaknesses of DDD
1. Steep Learning Curve: Requires a good understanding of DDD concepts and can be difficult to adopt for inexperienced teams.
2. Initial Overhead: The modeling process can be intensive and take more time in the early development phases.
3. Complexity in Small Projects: Can introduce unnecessary complexity in small or simple projects.
4. Need for Constant Collaboration: Requires ongoing collaboration between developers and domain experts, which can be challenging in some contexts.
5. Implementation Difficulty: Correctly identifying bounded contexts and domain division can be challenging and may require several iterations.
‍
And this is how the microservice looks that will be attached for you to take a look, clone it, and play around a bit. Read the README.md to see how to run it and all considerations to keep in mind.
https://github.com/eljoesb/ddd-kranio-blog
Ready to transform the architecture of your applications with modern and efficient approaches?
At Kranio, we have a team of experts in Hexagonal Architecture and Domain-Driven Design (DDD) who will help you implement scalable and maintainable microservice solutions. Contact us and discover how we can drive the technological evolution of your company.
Previous Posts

Google Apps Scripts: Automation and Efficiency within the Google Ecosystem
Automate tasks, connect Google Workspace, and enhance internal processes with Google Apps Script. An efficient solution for teams and businesses.

Augmented Coding vs. Vibe Coding
AI generates functional code but does not guarantee security. Learn to use it wisely to build robust, scalable, and risk-free software.
