← Back to articles

May 12, 2026

8 min read

Hexagonal Architecture Without Ceremony

How to separate UI, use cases, and infrastructure in a web app without turning every change into another layer.

Leer en espanol

The real problem is where decisions live

Most web apps become hard to change not because they lack a famous architecture, but because important logic ends up scattered across controllers, components, hooks, ORM models, and utility files. The system still compiles, but it stops being legible.

The useful version of hexagonal architecture is not about drawing six folders and introducing interfaces everywhere. It is about making one thing explicit: what part of the system makes business decisions, what part talks to the outside world, and how both connect without leaking into each other.

Put use cases and ports in the center

In a pragmatic web application, the center is usually a small set of use cases: create a workspace, invite a member, publish an article, recalculate a report. Each use case receives input, applies domain rules, and delegates external effects through ports.

A port only makes sense when it protects a real boundary: persistence, email, jobs, payments, cache, search, storage. If you need to swap providers later or test without the network, the port is already paying rent.

A small use case with explicit dependencies.

type PublishArticleInput = {
  articleId: string;
  publishedAt: Date;
};

interface ArticleRepository {
  findDraftById(id: string): Promise<Article | null>;
  save(article: Article): Promise<void>;
}

export async function publishArticle(
  input: PublishArticleInput,
  deps: { articles: ArticleRepository },
) {
  const article = await deps.articles.findDraftById(input.articleId);

  if (!article) throw new Error('Draft not found');

  article.publish(input.publishedAt);
  await deps.articles.save(article);
}

Keep adapters thin

Adapters should be boring. A route handler translates HTTP into use-case input. A repository maps persistence models to domain models. A UI component renders state and emits intent. As soon as an adapter starts making business decisions, the edge of the system has taken over.

This also matters on the frontend. A form should not decide which state transitions are valid for an order or subscription. It should send intent, receive a result, and render feedback. The domain is still the domain even when part of the app runs in the browser.

  • Use cases know rules and sequence.
  • Adapters know protocols and formats.
  • Infrastructure knows providers, SDKs, and operational details.

When it is worth it

You do not need a full hexagonal setup for a landing page with one form and two tables. But once the product has meaningful workflows, side effects, permissions, and rules that change often, separating decisions from the framework stops being purity and starts being a speed advantage.

The right signal is not repository size. It is how often the team has to touch five different places to change a single rule. When that is already happening, hexagonal architecture stops being an aspiration and becomes a tool.

More articles

Back to articles