# fn(args, deps) Is Programming to Interfaces — And That's How You Control Nondeterminism

By Jag Reehal, 2026-03-18. Canonical: https://arrangeactassert.com/posts/fn-args-deps-and-determinism/

<!-- Excerpt Start -->

Many software systems fail for one very boring reason.

Not because of microservices. Not because of monoliths. Not because of whatever methodology war is trending this week.

They fail because they are unpredictable.

If you make a change and you cannot reliably determine the impact, you cannot safely evolve the system. And when you cannot evolve it, it starts behaving like legacy.

> Determinism is the bridge between "works on my machine" and "works every time, everywhere."

`fn(args, deps)` gets you there — not because it is a clever trick, but because it makes boundaries explicit. Your logic programs to interfaces, which is what lets you control sources of nondeterminism.

<!-- Excerpt End -->

Dave Farley makes this argument better than almost anyone. If you have not seen his talk on determinism and evolutionary architecture, it is worth your time:

<iframe
  src="https://www.youtube.com/embed/uHatwKrYY_c"
  style="width:100%; aspect-ratio:16/9;"
  frameborder="0"
  allowfullscreen
></iframe>

What follows is not a recap of that talk. It is about one specific claim: that `fn(args, deps)` is a concrete, function-level way to implement the core of what Farley is describing.

## Determinism is the prerequisite

You cannot evolve what you cannot measure. You cannot measure what you cannot repeat. And you cannot repeat what you cannot reliably reproduce.

Flaky tests, intermittent builds, and concurrency heisenbugs turn your delivery pipeline from a learning system into release theater.

Determinism is the prerequisite for trust. Without it, your feedback loops degrade and your architecture rots quietly in the background.

With it, you can run thousands of experiments per day, detect unintended consequences, and move fast without gambling. That is an evolutionary capability.

Continuous delivery depends on pipelines, but pipelines only earn trust when the system beneath them is deterministic. The pipeline is just the amplifier.

## Separate the code that decides from the code that acts

Farley frames this as the deterministic core and the imperative shell.

The deterministic core is *reproducible logic under controlled inputs*. State in, decision out. No hidden database calls, no hidden clock, no hidden randomness.

The imperative shell is the messy part. Talk to the database, talk to the network, read the clock, execute side effects.

You can map this to “ports and adapters,” but the point here is simpler: “how do I make behavior predictable?”

If you have read the [earlier post on hexagonal architecture](/posts/fn-args-deps-and-hexagonal-architecture/), the structure is familiar. What changes here is the reason it matters.

## fn(args, deps) is programming to interfaces

This is the connection most people miss.

When you write a deps type, you are writing an interface. Not a Java-style `interface` with a keyword and a class that `implements` it. Just a type that describes what capabilities the function needs from the outside world.

```typescript
type GetExpiringItemsDeps = {
  now: () => number;
  findItemsExpiringBefore: (cutoff: Date) => Promise<Item[]>;
};
```

That type is the interface. The function programs to it. The [composition root](/posts/composition-roots-and-fn-args-deps/) decides what implements it.

```typescript
function expirationCutoff(
  args: { windowMs: number },
  deps: { now: () => number },
) {
  return new Date(deps.now() - args.windowMs);
}

async function getExpiringItems(
  args: { windowMs: number },
  deps: GetExpiringItemsDeps,
) {
  return deps.findItemsExpiringBefore(expirationCutoff(args, deps));
}
```

The function does not know whether `now` returns the real time or a frozen timestamp. It does not know whether `findItemsExpiringBefore` queries Postgres or returns a hardcoded array. It is deliberately indifferent.

That is what programming to interfaces means: your logic does not reach behind the contract. It stays on the surface—capabilities in, results out.

And that is what makes the logic *reproducible under controlled inputs*. Once sources of nondeterminism are explicit, you can control or replace them for the purpose of a call.

> The deps type is the interface. The function programs to it. That is all programming to interfaces ever needed to be.

## The clock example

Here is the simplest way to see it.

If your code calls `Date.now()` directly, you have pulled time into the middle of your logic. The same explicit input tomorrow can produce a different output, because time is now a hidden input. You *can* still test it, but tests get harder to make reliable and harder to replay exactly.

```typescript
// nondeterministic — the output depends on when you run it
async function getExpiringItems(windowMs: number) {
  const cutoff = new Date(Date.now() - windowMs);
  return db.findItemsExpiringBefore(cutoff);
}
```

Instead, inject a clock. Pass time as data. Treat `now` as an input.

```typescript
// reproducible under controlled deps — the output depends on what you pass in
function expirationCutoff(
  args: { windowMs: number },
  deps: { now: () => number },
) {
  return new Date(deps.now() - args.windowMs);
}

async function getExpiringItems(
  args: { windowMs: number },
  deps: GetExpiringItemsDeps,
) {
  return deps.findItemsExpiringBefore(expirationCutoff(args, deps));
}
```

Now something powerful happens. You can freeze time. You can fast-forward it. You can reproduce production bugs by replaying timestamps (assuming the rest of your dependencies are similarly controlled for the replay).

You have turned the universe into a parameter.

```typescript
it('finds items expiring within the window', async () => {
  const frozenNow = new Date('2026-03-19T12:00:00Z').getTime();

  const result = await getExpiringItems(
    { windowMs: 60_000 },
    {
      now: () => frozenNow,
      findItemsExpiringBefore: async () => [
        { id: '1', name: 'Trial', expiresAt: new Date('2026-03-19T11:59:30Z') },
      ],
    },
  );

  expect(result).toHaveLength(1);
  expect(result[0].name).toBe('Trial');
});
```

No module patching. No hoisted mocks. No test framework reaching into module internals. Just explicit test doubles passed as data.

This works because the function programs to the deps interface, not to the real clock or the real database.

## Why this matters for evolutionary architecture

When your core logic is written against explicit deps instead of reaching into the world, you can test it without monkey-patching, module interception, or elaborate scaffolding.

You pass in state. You assert on output. You can run thousands of tests in milliseconds.

Those are your fitness functions at scale. And when you have fitness functions you can trust, your architecture can evolve safely, one function at a time.

Determinism is not a coding trick. It is a systems property. `fn(args, deps)` does not create determinism by magic—it makes the boundaries around nondeterminism explicit. Once they are explicit, you can control them, replace them, and replay them.

## The chain

`fn(args, deps)` is programming to interfaces without the ceremony.

Programming to interfaces makes sources of nondeterminism explicit.

Explicit nondeterminism is controllable and testable.

Controllable, testable behavior is what makes deterministic systems possible.

Deterministic systems are what make evolutionary architecture possible.

That chain — from function signature to system capability — is why the pattern matters.