# fn(args, deps) and Hexagonal Architecture

By Jag Reehal, 2026-03-17. Canonical: https://arrangeactassert.com/posts/fn-args-deps-and-hexagonal-architecture/

<!-- Excerpt Start -->

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.

<div>
  <img
    src="/static/images/fn-args-deps-and-hexagonal-architecture.png"
    alt="fn(args, deps) and hexagonal architecture"
    class="w-full h-auto bg-white"
  />
</div>

You may also hear this called "ports and adapters" — same idea, different name. The port is what a function needs; the adapter is what fulfils it.

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:

```typescript
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.

<!-- Excerpt End -->

## 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:

- HTTP
- a CLI
- a queue consumer
- a cron job
- a test

And you do not want it tightly coupled to:

- a specific database client
- a specific mail provider
- a framework request object
- a logger singleton
- process-wide global state

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.

```typescript
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:

- `args` is what the use case needs from the caller
- `deps` is what the use case needs from the outside world

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:

- inbound adapters
- outbound adapters
- ports
- the application core

Those ideas can sound bigger than they are.

With `fn(args, deps)`, the mapping is usually straightforward:

- `args` comes from an inbound adapter
- `deps` is implemented by outbound adapters
- the function body is the application core
- the function signature is the boundary

Those adapters get assembled at the application edge — your [composition root](/posts/composition-roots-and-fn-args-deps/).

For example:

- an HTTP handler can translate a request into `args`
- a Postgres repository can implement part of `deps`
- an email provider can implement part of `deps`
- the use case itself stays unaware of Express, Fastify, Postgres, or SES

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:

```typescript
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:

- reads transport-specific input
- calls the use case
- translates the result back to transport-specific output

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.