Arrange Act Assert

Jag Reehals thinking on things, mostly product development

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

18 Mar 2026

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.

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:

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, 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.

type GetExpiringItemsDeps = {
  now: () => number;
  findItemsExpiringBefore: (cutoff: Date) => Promise<Item[]>;
};

That type is the interface. The function programs to it. The composition root decides what implements it.

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.

// 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.

// 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.

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.

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
  5. How fn(args, deps) supports SOLID-style design in TypeScript
  6. What `fn(args, deps)` actually gives you in DDD-style TypeScript
  7. fn(args, deps) — Bringing Order to Chaos Without Breaking Anything
  8. fn(args, deps) Is Programming to Interfaces — And That's How You Control Nondeterminism (this post)
  9. fn(args, deps) and Composition — Know When to Nest the Dolls
fn-args-deps typescript determinism architecture fn-args-deps-series