2025-01-23 6 min read

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

code
PaymentProcessor
in isolation. You test
code
PostgresPaymentRepository
with an actual database. You never test them together because they're deliberately decoupled.

Legitimately 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:

  1. Do you have complex business rules that need isolated testing? If yes, hexagonal makes sense.
  2. Will your team actually swap infrastructure? Not "could we"—will you?
  3. Does your codebase have enough volume to justify the structure? Microservices often do; a utility script doesn't.
  4. 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.

Share
LP

LavaPi Team

Digital Engineering Company

All articles