Hexagonal Architecture
Hexagonal architecture (ports and adapters) isolates business logic from external concerns. The domain doesn't know about databases, APIs, or UI frameworks — it only knows about ports (interfaces) that adapters implement.
The Core Idea
┌─────────────────────────────────────┐
│ ADAPTERS (outer) │
│ ┌───────────────────────────────┐ │
│ │ PORTS (interfaces) │ │
│ │ ┌─────────────────────────┐ │ │
│ │ │ DOMAIN (core) │ │ │
│ │ │ Business logic only │ │ │
│ │ └─────────────────────────┘ │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘
Dependency rule: Code flows inward. Domain knows nothing about adapters. Adapters depend on ports. Ports define what the domain needs.
Nx Library Structure
Map hexagonal layers to Nx libraries:
libs/
domain/ → Entities, business rules, use cases
entities/ → Core business objects
use-cases/ → Application-specific business rules
ports/ → Interfaces the domain needs
repositories/ → Data access contracts
services/ → External service contracts
adapters/
primary/ → Driving adapters (UI, API routes)
ui/ → React components, pages
api/ → Next.js API routes, server actions
secondary/ → Driven adapters (implementations)
db/ → Drizzle/Prisma repositories
external/ → Third-party API clients
apps/
web/ → Next.js app (thin shell)
Layer Responsibilities
| Layer | Contains | Depends On |
|---|---|---|
| Domain | Entities, use cases, business rules | Nothing external |
| Ports | Repository interfaces, service contracts | Domain types only |
| Primary Adapters | UI components, API routes, controllers | Ports + Domain |
| Secondary Adapters | DB implementations, API clients | Ports + Domain |
Next.js Integration
Server Actions as Primary Adapters
// apps/web/app/actions/contact.ts (Primary Adapter)
'use server'
import { createContact } from '@myorg/domain/use-cases'
import { contactRepository } from '@myorg/adapters/db'
export async function createContactAction(formData: FormData) {
const input = parseContactInput(formData)
return createContact(input, { contactRepository })
}
Use Cases in Domain
// libs/domain/use-cases/createContact.ts
import type { ContactRepository } from '@myorg/ports/repositories'
import { Contact } from '@myorg/domain/entities'
export async function createContact(
input: CreateContactInput,
deps: { contactRepository: ContactRepository }
) {
const contact = Contact.create(input)
return deps.contactRepository.save(contact)
}
Ports as Interfaces
// libs/ports/repositories/ContactRepository.ts
import type { Contact } from '@myorg/domain/entities'
export interface ContactRepository {
save(contact: Contact): Promise<Contact>
findById(id: string): Promise<Contact | null>
findAll(): Promise<Contact[]>
}
Secondary Adapters Implement Ports
// libs/adapters/db/DrizzleContactRepository.ts
import type { ContactRepository } from '@myorg/ports/repositories'
import { db, contacts } from './schema'
export const drizzleContactRepository: ContactRepository = {
async save(contact) {
const [result] = await db.insert(contacts).values(contact).returning()
return result
},
// ... other methods
}
Nx Boundary Enforcement
Configure @nx/enforce-module-boundaries in .eslintrc.json:
{
"rules": {
"@nx/enforce-module-boundaries": [
"error",
{
"depConstraints": [
{ "sourceTag": "layer:domain", "onlyDependOnLibsWithTags": [] },
{ "sourceTag": "layer:ports", "onlyDependOnLibsWithTags": ["layer:domain"] },
{ "sourceTag": "layer:adapters", "onlyDependOnLibsWithTags": ["layer:ports", "layer:domain"] },
{ "sourceTag": "layer:app", "onlyDependOnLibsWithTags": ["layer:adapters", "layer:ports", "layer:domain"] }
]
}
]
}
}
Now architecture violations fail the build.
Why This Matters
| Benefit | How Hex + Nx Delivers |
|---|---|
| Testability | Mock ports, test domain in isolation |
| Swappability | Change DB or API without touching domain |
| AI-assisted dev | Clear boundaries help AI understand structure |
| Team scaling | Teams own layers, not features |
| Build speed | Nx only rebuilds affected libraries |
Context
- Clean Architecture — Detailed implementation patterns
- Next.js Clean Architecture — Next.js-specific patterns
- Nx Monorepo — Build system setup