Hexagonal Architecture for Backend Services: Worth the Ceremony?
Hexagonal architecture promises clean separation and testability, but does the complexity justify itself? We break down when it actually pays off.
Hexagonal Architecture for Backend Services: Worth the Ceremony?
You've heard the pitch: hexagonal architecture (ports and adapters) makes your code independent, testable, and future-proof. Your domain logic lives untouched at the center while infrastructure concerns orbit around it. Sounds perfect. Then you actually build it, and suddenly you're writing adapter interfaces for everything, managing dependency injection containers, and wondering if you've over-engineered a CRUD endpoint.
The honest answer? Hexagonal architecture has real value—but only under specific conditions.
When Hexagonal Actually Delivers
Complex Domain Logic That Needs Protection
If your service contains non-trivial business rules, hexagonal shines. When your domain logic is insulated from database schemas, message queues, and HTTP frameworks, you can test it without mocking infrastructure. At LavaPi, we've seen this pattern accelerate development when teams maintain consistency across multiple teams working on interconnected services.
Consider a payment processing service where business rules around fraud detection, reconciliation, and compliance matter deeply. Your core logic shouldn't know whether payments come from a REST API, a webhook, or a scheduled job:
typescript// Domain layer - completely framework agnostic export class PaymentProcessor { process(payment: Payment): ProcessResult { if (payment.amount > this.riskThreshold) { return this.flagForReview(payment); } return this.authorize(payment); } } // Port definition export interface PaymentRepository { save(payment: Payment): Promise<void>; findById(id: string): Promise<Payment | null>; } // Adapter for PostgreSQL export class PostgresPaymentRepository implements PaymentRepository { constructor(private db: Database) {} async save(payment: Payment): Promise<void> { await this.db.query( 'INSERT INTO payments (id, amount, status) VALUES ($1, $2, $3)', [payment.id, payment.amount, payment.status] ); } }
You test
PaymentProcessorPostgresPaymentRepositoryLegitimately Changing Requirements
Hexagonal earns its keep when you genuinely anticipate (or experience) infrastructure swaps. If you might move from PostgreSQL to DynamoDB, or from direct database access to an event stream, the abstraction prevents rewriting your domain logic.
But be honest: Are you actually changing infrastructure? Or are you speculating? If your last three production years used PostgreSQL without question, hexagonal isn't protecting you—it's penalizing velocity.
When It's Just Overhead
CRUD Services with Thin Logic
A simple user service that validates input, writes to a database, and returns JSON doesn't benefit from hexagonal architecture. You'll create four layers to handle what could be one. The "ceremony" becomes the entire codebase:
python# Probably unnecessary class UserDomain: def validate(self, data): return len(data.get('email', '')) > 0 class UserPort(ABC): @abstractmethod def create(self, user): pass class UserAdapter(UserPort): def create(self, user): db.insert('users', user)
Compare that to a straightforward approach:
python# Honest and functional @app.post('/users') def create_user(data: UserSchema): if not data.email: return error() db.insert('users', data.dict()) return success()
The second scales perfectly fine until your domain logic actually becomes interesting.
The Real Decision Framework
Don't ask "Is hexagonal architecture good?" Ask these questions:
- Do you have complex business rules that need isolated testing? If yes, hexagonal makes sense.
- Will your team actually swap infrastructure? Not "could we"—will you?
- Does your codebase have enough volume to justify the structure? Microservices often do; a utility script doesn't.
- Can your team maintain discipline with the pattern? Without discipline, you get the complexity without the benefits.
Hexagonal architecture is a tool for solving specific problems: isolating complex logic and managing legitimate infrastructure uncertainty. If you have those problems, implement it thoughtfully. If you're building a straightforward service with clear infrastructure, take the simpler path and spend your mental energy on problems that actually exist.
The ceremony isn't pointless—but it's not magic, either.
LavaPi Team
Digital Engineering Company