Layered Architecture is an important starting point for protecting the domain model. But in real code, even when layers appear to flow in one direction, the domain is often still strongly tied to frameworks or databases.
Hexagonal Architecture, or Ports and Adapters Architecture, helps us look at this problem more clearly. The core idea is simple. Place the domain at the center, and handle connections to the outside world through ports and adapters.
This structure fits well with DDD. If DDD values the language and rules of the domain model, Hexagonal Architecture provides a code structure that prevents that model from being pulled by external technology.
The domain is at the center
In diagrams of Hexagonal Architecture, the application and domain are in the center, and several adapters attach around the outside.
Web Controller -> Application / Domain -> Database Adapter
CLI Command -> Application / Domain -> Payment Adapter
Message Listener -> Application / Domain -> Event Publisher
What matters is that the outside world does not directly dominate the domain. Whether it is a web request, a message, or a batch job, it is only one way to enter an application use case.
Likewise, databases, payment APIs, and message brokers are not the center of the domain. They are implementations outside the domain that provide functions the domain needs.
This perspective differs from dividing code around technology.
controller
service
repository
entity
This structure is not always bad. But over time, web frameworks and database structure easily become the center. Hexagonal Architecture moves the center back to the domain and use cases.
Port is intent, Adapter is implementation
The key words in Hexagonal Architecture are Port and Adapter.
Port is the boundary the application defines to interact with the outside world. Adapter is the implementation that connects that Port to actual technology.
Suppose a subscription renewal use case must request payment. From the application’s perspective, what matters is the intent: “request payment and receive a result.” Which payment gateway is used, whether it uses HTTP API, and what JSON is exchanged are details.
public interface PaymentPort {
PaymentResult pay(PaymentRequest request);
}
This is the Port. The implementation that calls the real payment gateway is the Adapter.
public class PgPaymentAdapter implements PaymentPort {
public PaymentResult pay(PaymentRequest request) {
PgResponse response = pgClient.requestPayment(toPgRequest(request));
return paymentTranslator.toPaymentResult(response);
}
}
The domain and application only need to know PaymentPort. Details of the external API are trapped inside the Adapter.
Inbound Adapter and Outbound Adapter
Adapters can be broadly divided into two kinds.
Inbound Adapter brings external requests into the application. REST Controllers, GraphQL Resolvers, message listeners, CLI commands, and batch schedulers belong here.
Outbound Adapter is an implementation called when the application uses an external system. Database Repository implementations, payment API clients, email senders, and message publishers belong here.
For a subscription cancellation use case, the structure could look like this:
CancelSubscriptionController
-> CancelSubscriptionUseCase
-> SubscriptionRepository
-> JpaSubscriptionRepository
The Controller is an Inbound Adapter. CancelSubscriptionUseCase is an application entry port or service. SubscriptionRepository is a Port for storage access, and JpaSubscriptionRepository is an Outbound Adapter.
In this structure, neither the web nor the database is the center. The use case and domain model are at the center.
A structure that is easy to test
One major advantage of Hexagonal Architecture is that it is easy to test. When external technology is pushed behind Ports, the application and domain can be tested with fake implementations.
FakePaymentPort paymentPort = new FakePaymentPort(PaymentResult.succeeded());
InMemorySubscriptionRepository repository = new InMemorySubscriptionRepository();
RenewSubscriptionUseCase useCase = new RenewSubscriptionUseCase(repository, paymentPort);
This allows us to verify the use case without a real payment gateway or database. What matters is not test convenience itself. Ease of testing is a signal that the domain and application are less tied to external technology.
Of course, not every test should use fakes. Adapters themselves need separate integration tests. Payment API translation, JPA mapping, and message publishing should be verified with the real technology.
The key is that we can separate the target and purpose of tests.
Why it fits well with DDD
What matters in DDD is protecting the language and rules of the domain model. Hexagonal Architecture structurally supports this goal.
External API models are translated into internal models in Adapters. Database mapping is handled in Repository Adapters. Web request DTOs are converted into Application Commands. Message formats are interpreted in message Adapters.
As a result, the Domain Layer does not need to know things such as:
HTTP request format
JSON field names
DB table names
Payment gateway response codes
Message broker topic names
Framework annotations
The domain model only needs to know the language it should know.
subscription.renew(paymentResult, now);
subscription.cancelByMember(reason, now);
subscription.changePlan(targetPlan, policy, now);
This is where DDD and Hexagonal Architecture meet.
How is it different from Layered Architecture?
Layered Architecture and Hexagonal Architecture are not opposing concepts. In many cases, they can be used together.
Layered Architecture is useful for explaining responsibilities of layers. Dividing code into Presentation, Application, Domain, and Infrastructure makes the large structure easier to understand.
Hexagonal Architecture emphasizes the boundary between the outside world and the internal application more strongly. It explicitly handles various input and output Adapters.
In a layered structure, an Application -> Infrastructure dependency often appears naturally. From a Hexagonal perspective, we can invert this with a Port.
Application depends on PaymentPort.
Infrastructure's PgPaymentAdapter implements PaymentPort.
This separates application intent from technical implementation.
It can become excessive abstraction
Hexagonal Architecture is not always the answer. In a small CRUD service, separating every Repository, every external call, and every input path into Port and Adapter can make code unnecessarily complicated.
What matters is whether boundaries are needed to handle complexity.
Hexagonal structure is especially helpful in cases like these:
The Core Domain must be protected from external technology.
External systems may change often.
The same use case is called from several input methods.
External dependencies must be easily replaceable in tests.
There is a large language difference between legacy or external models and internal models.
Conversely, not every simple admin CRUD feature needs a complete Hexagonal structure. Architecture must fit the problem.
Closing
Hexagonal Architecture is a structure that places the domain at the center. The outside world connects through ports and adapters. Web, databases, message brokers, and external APIs are important, but they are not the center.
From a DDD perspective, the value of this structure is clear. It prevents the domain model from being pulled by the language of external technology. External requests are translated into Application Commands, external API responses are translated into internal domain results, and storage implementations hide behind Repository Ports.
But Hexagonal Architecture is a means, not a goal. Wrapping every piece of code in ports and adapters is not the objective. The objective is to protect the domain model, make use cases clear, and build a structure that is less shaken by changes in external technology.
Good architecture helps the domain speak better. Hexagonal Architecture is a strong option for that goal.