Best package organization for hexagonal architecture?

Estimated reading: 8 minutes 7 views

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.
Share this Doc

Best package organization for hexagonal architecture?

Or copy link

CONTENTS
Scroll to Top