Skip to content
Riadh Mnasri
← Back to blog
3 min read

Hexagonal, Clean Architecture, DDD: telling them apart for good

On a recent engagement, a developer asked me whether his project was "in hexagonal or in DDD." The question doesn't quite make sense as asked: they are two answers to two different problems, and nothing stops you from combining them. This confusion comes up often enough to be worth untangling properly, which is what I ended up formalizing in archi3, a bilingual teaching site with one article per approach and a searchable glossary.

The problem each one solves#

ApproachProblem solvedCore concept
Hexagonal architectureIsolating the business logic from technical details (database, framework, external API)Ports and adapters
Clean ArchitectureStructuring dependencies so they always point inwardThe dependency rule, four circles
Domain-Driven DesignModeling a complex business domain with a shared vocabularyThe ubiquitous language, aggregates

None of the three tells you how to name your classes or organize your folders. These are dependency and modeling rules, not naming conventions, which explains a good part of the confusion: two "hexagonal" projects can have a completely different folder structure and both be correct.

Hexagonal architecture: ports and adapters#

The idea fits in one sentence: the business logic defines interfaces (the ports), and anything technical implements them (the adapters). A database, a REST API, a message queue: these are interchangeable adapters, never dependencies of the domain.

kotlin
// Port: defined by the domain, no technical dependency
interface CollateralRepository {
    fun findByTradeId(tradeId: TradeId): Collateral?
}
 
// Adapter: implements the port, knows the technology
class PostgresCollateralRepository(
    private val jdbcTemplate: JdbcTemplate,
) : CollateralRepository {
    override fun findByTradeId(tradeId: TradeId): Collateral? {
        // SQL query, mapping, etc.
        TODO()
    }
}

The most common trap: defining a port that already exposes technical details (a ResultSet type, Spring Data pagination) in its signature. At that point the port isn't isolating anything anymore, it's just moving the problem from one file to another.

Clean Architecture: the dependency rule#

Clean Architecture formalizes the same intuition in a different shape: four concentric circles (entities, use cases, interface adapters, frameworks), with one absolute rule, a dependency arrow that never points outward. The middle circle knows nothing about the one above it.

In practice, on a Kotlin/Spring Boot project, that translates to: the domain module depends on no Spring annotation, no JPA class, no @RestController. If your IDE offers to import org.springframework.* into a domain class, that's a signal the rule has just been broken.

Domain-Driven Design: modeling, not just isolating#

Where hexagonal and Clean Architecture talk about dependencies, DDD talks about modeling. The ubiquitous language (the shared business vocabulary between developers and domain experts) and aggregates (transactional consistency boundaries) answer a different question: how to faithfully represent a complex domain in code, not just where to place the layers.

kotlin
// A DDD aggregate: "Trade" consistency is guaranteed
// by the root, never by accessing Collateral directly
class Trade private constructor(
    val id: TradeId,
    private val collaterals: MutableList<Collateral>,
) {
    fun postCollateral(collateral: Collateral) {
        require(collateral.currency == this.currency) {
            "Collateral currency incompatible with the trade"
        }
        collaterals.add(collateral)
    }
}

That's why DDD combines so naturally with hexagonal: DDD says how to model the inside of the business circle, hexagonal says how to protect that inside from everything else. These aren't two rival schools, they're two answers to two questions that come up one after the other.

The real trap: treating them as mutually exclusive#

The costliest confusion isn't mixing up the vocabulary, it's believing you have to choose. On MissionMatch, the domain is modeled with DDD aggregates and a ubiquitous language, structured with a hexagonal architecture, with a domain/application/infrastructure separation that respects Clean Architecture's dependency rule. All three coexist because they answer different questions: how to model, how to structure dependencies, how to isolate the technical layer.

The question to ask is therefore never "which of the three." It's: where is the shared business vocabulary (DDD), where are my dependency boundaries (hexagonal), and does that boundary respect a consistent direction (Clean Architecture). Answering all three at once isn't a contradiction, it's the normal case.