Arrange Act Assert

Jag Reehals thinking on things, mostly product development

fn(args, deps) and Hexagonal Architecture

17 Mar 2026

Hexagonal architecture often gets introduced with a lot of diagrams, vocabulary, and ceremony.

Ports. Adapters. Application core. Inbound and outbound boundaries.

That framing is useful, but it can also make the idea feel more abstract than it really is.

fn(args, deps) and hexagonal architecture

A simpler way to understand it is this:

Hexagonal architecture is mostly about keeping business logic independent from delivery and infrastructure details.

One practical way to do that in plain TypeScript is this shape:

fn(args, deps);

Where args is the input for this use case, and deps is the set of external capabilities it needs. You can read that as: data in, capabilities in.

In plain TypeScript, fn(args, deps) is a simple way to implement the core idea of hexagonal architecture without turning the pattern into ceremony.

The real point of hexagonal architecture

A lot of explanations make hexagonal architecture sound like the point is drawing a boundary around "the domain."

That is not wrong, but it is incomplete.

The real point is to stop business logic from being tangled up with transport, frameworks, and infrastructure.

You do not want your core use case to care whether it was called from:

And you do not want it tightly coupled to:

That is the architectural problem hexagonal architecture is trying to solve.

A use case can just be a function

In plain TypeScript, a use case does not need to be a class to have a boundary.

It can just be a function.

type CreateUserArgs = {
  name: string;
  email: string;
};

type CreateUserDeps = {
  saveUser: (input: { name: string; email: string }) => Promise<User>;
  sendWelcomeEmail: (user: User) => Promise<void>;
  log: (message: string, meta?: unknown) => void;
};

export async function createUser(args: CreateUserArgs, deps: CreateUserDeps) {
  deps.log('Creating user', { email: args.email });

  const user = await deps.saveUser(args);
  await deps.sendWelcomeEmail(user);

  return user;
}

That function already has the important boundary:

That is a very direct way to express the "inside versus outside" split that hexagonal architecture cares about.

args and deps map cleanly to the hexagon

This is the useful connection.

In hexagonal architecture, people often talk about:

Those ideas can sound bigger than they are.

With fn(args, deps), the mapping is usually straightforward:

For example:

That is hexagonal architecture in practice, not just in a diagram.

The HTTP layer becomes an adapter, not the center

Here is a simple HTTP adapter:

import { createUser } from '../application/create-user';
import { makeCreateUserDeps } from '../composition-root';

export async function postUsersHandler(req: Request, res: Response) {
  const deps = makeCreateUserDeps();

  const user = await createUser(
    {
      name: req.body.name,
      email: req.body.email,
    },
    deps,
  );

  res.status(201).json({
    id: user.id,
    name: user.name,
    email: user.email,
  });
}

The handler does three things:

That is adapter work.

The business logic is not in the handler. The handler is not the center of the system. It is just one way into the core.

That is one of the biggest practical wins of hexagonal architecture: the delivery mechanism becomes replaceable.

fn(args, deps) Series

  1. If You Only Enforce One Rule for AI Code, Make It fn(args, deps)
  2. Why Matt Pocock Is Right About Making Codebases AI Agents Love
  3. Composition Roots and fn(args, deps)
  4. fn(args, deps) and Hexagonal Architecture (this post)
  5. How fn(args, deps) supports SOLID-style design in TypeScript
  6. How fn(args, deps) supports DDD and aggregate roots in TypeScript
  7. fn(args, deps) — Bringing Order to Chaos Without Breaking Anything
typescript coding architecture hexagonal-architecture di