Clean & Hexagonal Architecture
Organising code so the core domain has no dependency on frameworks, databases, or delivery mechanisms.
Overview
Clean Architecture (Robert C. Martin) and Hexagonal Architecture (Alistair Cockburn, also called Ports and Adapters) both organise code so the core domain logic has zero dependencies on frameworks, databases, UI, or any external system. The domain sits at the centre; adapters translate between the domain and the outside world. This makes the domain independently testable and the infrastructure swappable.
Origin
Hexagonal Architecture was introduced by Alistair Cockburn in 2005. Robert C. Martin's Clean Architecture (2012 blog post, 2017 book) synthesised it with Onion Architecture (Palermo, 2008) and DDD principles. The patterns address the same problem: preventing infrastructure concerns from polluting business logic.
Examples
Ports and Adapters: domain isolated from infrastructure
# Port (interface the domain depends on, just a Ruby duck type)
module UserRepository
def find(id) = raise NotImplementedError
def save(user) = raise NotImplementedError
end
# Domain use case, depends on the port, not any concrete DB
class RegisterUser
def initialize(repo) # injected, never instantiated here
@repo = repo
end
def call(email:, password:)
raise "Email taken" if @repo.find_by_email(email)
user = User.new(email: email, password_hash: BCrypt::Password.create(password))
@repo.save(user)
user
end
end
# Adapter: the real PostgreSQL implementation
class PGUserRepository
include UserRepository
def find(id) = User.from_row(DB[:users].where(id: id).first)
def save(user) = DB[:users].insert(user.to_h)
end
# In tests, swap for an in-memory adapter, no DB needed
class InMemoryUserRepository
include UserRepository
def initialize = (@store = {})
def find(id) = @store[id]
def save(user) = @store[user.id] = user
endUse Cases
- 01Systems that must remain testable without a running database or external APIs
- 02Long-lived codebases where the persistence layer or framework may need to change
- 03Microservices: each service's core logic must be isolated from transport (HTTP, gRPC, queues)
- 04Domain-complex applications (finance, logistics) where business rules must not be entangled with ORM code
When Not to Use
- //Simple CRUD applications where the domain logic is thin, the indirection has a maintenance cost
- //Small teams or greenfield projects where the architecture overhead slows iteration before the domain is understood
- //When the framework (Rails) already provides the structure and the business domain is simple enough to live in models and controllers
Technical Notes
- The Dependency Rule: source code dependencies always point inward. The domain never imports an adapter
- Use cases (application services) orchestrate the domain; they do not contain domain logic themselves
- Entities and Value Objects are the core: they have no dependencies at all, no Rails, no ActiveRecord, no HTTP
- The cost is indirection: more files, more interfaces, more mapping. The benefit is that each layer is independently testable and replaceable
More in Architecture