Best package organization for hexagonal architecture?
The best package organization places domain logic in a core package, while external adapters and interface definitions reside in separate boundary packages. This structure ensures the core remains isolated from implementation details, allowing ports to define contracts without depending on the technology stack.
Core Principles of Hexagonal Package Layout
Successful modularization begins with understanding the direction of dependencies. In a clean architecture, information flows outward. The inner circles are pure logic, and the outer circles are specific implementations.
You must strictly separate the “what” from the “how”. The what defines the use cases and domain entities. The how defines database drivers, web frameworks, and external APIs.
Structuring your hexagonal architecture packages requires a strict adherence to dependency rules. No internal class should reference a library like JPA, Spring Web, or Kafka directly.
This separation creates a modular foundation where business logic remains stable even as infrastructure changes. You can swap out an implementation without touching the core domain logic.
Defining the Core Domain Package
The central package holds your business value. It contains entity classes, value objects, and the use case orchestrators. This is the “Heart” of your application.
- Domain Entities: Classes representing business reality without infrastructure logic.
- Use Cases: Interfaces and implementations that manage business rules.
- Domain Events: Internal state changes that other parts of the system might observe.
In this specific package, you do not import any framework classes. You only import standard libraries. If you see a dependency on a web framework here, you have violated the boundary.
Structuring the Port Interfaces
Ports define the contracts that the core expects. These are often named with suffixes like “Repository”, “Service”, or “Port”. In Java, these are typically interfaces or abstract classes.
You organize ports into two distinct groups based on their direction:
- Driven Ports: These are interfaces the core calls to get data or trigger actions. They are often implemented by the infrastructure layer.
- Driving Ports: These are entry points where external systems trigger logic, such as controller endpoints or CLI commands.
Placing these interfaces in a sub-package within the core keeps the definitions close to the logic that consumes them. It prevents the core from becoming a collection of unrelated interfaces.
Procedural Guide to Folder Structure
Follow these steps to translate the theoretical model into a physical file structure. This procedural approach ensures consistency across your team.
Step 1: Create the Core Package
Action: Create a root package named after your application domain, e.g., com.company.project.domain.
Result: You have a safe container for business logic. All entity classes and core use cases reside here.
Step 2: Create the Adapter Packages
Action: Create sub-packages for “inbound” and “outbound” adapters under the root package.
Result: You establish boundaries. Inbound adapters handle incoming requests, while outbound adapters handle outgoing data persistence.
Step 3: Organize Interface Definitions
Action: Place the interface definitions inside the core package, often in a sub-folder named ports or interfaces.
Result: The core knows its requirements without knowing the implementation details of its dependencies.
Step 4: Implement Concrete Adapters
Action: Create concrete classes that implement the port interfaces. Place these in the adapter packages.
Result: The infrastructure layer is isolated. You can change the database driver without touching the domain package.
Managing Dependencies Between Modules
The most critical aspect of hexagonal architecture packages is how you handle dependencies. You must prevent the core from polluting its dependencies.
Driving Ports and External Entry Points
Driving ports sit on the boundary between the outside world and the core. They accept data and delegate to the core logic.
Typically, these are web controllers or command-line interfaces. They depend on the core, but the core does not know about them.
If you have a REST API, your controller package should import the core package. This dependency is allowed. It flows inward from the “driving” layer.
Driven Ports and Persistence Layers
Driven ports are interfaces defined by the core. They are implemented by the infrastructure layer.
For example, if your core needs to save a user, it defines a UserRepository interface. The infrastructure layer implements this interface using JPA or Hibernate.
The infrastructure layer depends on the core. The core does not depend on JPA. This inversion of control is the definition of the Hexagonal pattern.
Common Misconceptions About Hexagonal Packaging
Many developers struggle with the implementation due to common misunderstandings about how the packages should interact.
Misconception 1: The Core Must Not Depend on Anything
It is often thought that the core package should be completely isolated. However, the core must depend on the interface definitions of its ports.
While the core cannot depend on database drivers, it must define the contracts. These definitions are part of the core package.
Misconception 2: Adapters Belong in the Same Package as the Core
Do not put the implementation of a repository in the same package as its interface if it contains framework-specific code.
Keep the interface in the core and the implementation in the infrastructure package. This separation allows you to swap implementations easily.
Misconception 3: Ports Are Just Interfaces
While ports are typically interfaces, the architecture also accommodates classes that manage state. However, state management must remain within the core domain.
Any state that is specific to a technology (like a Hibernate Session) must reside outside the core package.
Scaling Hexagonal Packages for Large Models
As your application grows, the complexity of hexagonal architecture packages increases. You need strategies to manage large models effectively.
Vertical Slice Architecture
Instead of organizing packages by layer (Controller, Service, Repository), organize by feature or vertical slice.
Each feature folder contains its own domain entities, ports, and adapters. This keeps related logic together and reduces coupling.
This approach works exceptionally well with hexagonal architecture packages because each feature is self-contained.
Module-Based Grouping
Group related entities and ports into cohesive modules. For example, a “Payment” module should contain all payment-related logic and adapters.
Modules should be independent. If you delete the Payment module, the rest of the system should still function.
Shared Kernel Package
Create a shared package for common elements that multiple modules need. Examples include global error codes or shared utilities.
Be cautious with shared packages. If they become too large, they become a “god package” and reintroduce coupling.
Addressing Modularization Challenges
Real-world systems often face issues when applying strict modularization rules. Here are solutions to common problems.
Handling Circular Dependencies
Circular dependencies occur when Package A depends on Package B, and B depends on Package A.
To fix this, extract the shared interface into a third, neutral package. Both original packages then depend on this new interface package.
This ensures that the dependency flow remains linear and does not create a loop.
Shared Data Transfer Objects
Adapters often need to exchange data with the core. Using complex DTOs can create tight coupling.
Define DTOs in the adapter package. The core should only receive primitive types or domain entities.
If the core needs specific data, define a specific port for that data rather than passing a generic map or object.
Mapping Packages to Code Structure
Translating the conceptual model to actual code is a critical step for developers.
Package Naming Conventions
Use a consistent naming convention to reflect the architecture layers. Common names include domain, application, infrastructure, and interface.
This makes the structure visible at a glance in your IDE. It helps new developers understand the layout immediately.
Package Visibility and Access Control
Use package-private visibility for interfaces in the core to enforce strict access control.
This prevents external code from accessing core interfaces directly without going through the correct adapters.
It enforces the contract that only the designated adapters can interact with the core system.
Advanced: Testing Hexagonal Structures
Testing your package organization validates the modularity of your architecture.
Unit Testing the Core
When testing the core package, you must use mocked versions of your ports.
Your test fixtures should implement the interfaces but do not need the infrastructure code. This ensures you are testing logic, not side effects.
Integration Testing Adapters
Adapters require integration testing to verify they connect correctly with external systems.
These tests should run separately from the core tests. They should focus on the interaction between the adapter and the technology layer.
Summary of Package Organization Strategy
Organizing hexagonal architecture packages requires a disciplined approach to dependencies. The core must remain pure.
Ports define the contracts, and adapters implement them. This clear separation allows for flexible development.
By following these structural guidelines, you ensure your codebase remains maintainable and testable.
- Keep domain logic isolated in the core package.
- Place ports and adapters in separate boundary packages.
- Ensure dependencies flow outward from the core.
- Use modularization to group related features together.
- Test core logic with mocks and adapters with integration tests.