now() is a clock you did not inject

You injected a Clock, pinned it in every test, and then wrote published_at = now() in a SQL statement. There are two clocks in your service and only one of them takes instructions.

You already know why a test should not read the wall clock, and your service takes a Clock as a dependency. Every test pins it. Nothing sleeps, nothing retries, and no suite has ever failed because it ran across midnight.

Then somewhere there is a statement like this, and the discipline stops at the connection:

update outbox set published_at = now() where id = :id
The clock you inject and the one you did not Tests pin a clock bean, which the service and its rules use. A SQL statement calling now() takes its time from the database instead, so rows written that way carry a time no test controls, and comparisons between the two disagree. what your tests pin the Clock bean rules and use cases timestamps you set what it does not reach now() in SQL columns stamped by the database compare these two and the answer is never what the test asked for
Injecting a clock does not stop the database having one. Any column stamped by SQL carries a time no test in your suite chose. Illustrative — the shape of the argument, not measured data.

How it shows up, which is not as flakiness

A second clock does not make tests flaky. Flaky would be easier: you would notice.

It shows up when something compares a time you set against a time the database set. Here is the one that caught me. A retention job deletes rows published longer ago than a window. The test pins the clock to a Monday in March, marks a row published, and asserts the row is deleted:

expected: 1
 but was: 0

The pruner asked for rows published before March minus seven days. The row carried the real date, six months later. Nothing was ever going to be deleted, the assertion pointed at the pruner, and the pruner was correct.

That is the flavour of this bug. Not a race, not a rare failure. A comparison between two clocks that will never agree, failing the same way every run, in a place that looks like arithmetic.

The list is longer than you think

Injecting Clock covers the code you wrote. These are the ones that write a time without asking:

  • now(), current_timestamp, localtimestamp in any statement
  • a column with default now()
  • @CreationTimestamp and @UpdateTimestamp
  • a trigger that stamps a row on insert or update
  • updated_at maintained by an ORM’s auditing support
  • the broker’s own timestamp on a message, if you ever read it back

Each of those is a clock. Each is invisible to a test that pins yours, and each is fine until something compares it to a value your code produced.

What to do instead

Pass the instant in. It is a smaller change than it sounds, because the code that knows what time it is already has the clock.

// the port takes the moment, so one clock decides
void markPublished(UUID id, Instant at);

// and the caller has the injected one
this.outbox.markPublished(stored.id(), this.clock.instant());

There is a real objection: the database’s clock is the same everywhere, and yours is per process, so on several instances they can differ by whatever your NTP drift is. That is true and it is usually the smaller problem. Drift between hosts is bounded and measurable; a value no test can control is neither.

If you genuinely need the database’s time for correctness, take it deliberately: select it, pass it around like any other value, and let a test replace it. What you are avoiding is not the database’s clock. It is the second one.

The half of this that is about reading tests

My first fix was wrong in an instructive way. I made the timestamp injectable, re-ran, and the same test still failed, so I assumed the fix was incomplete.

It was not. The test was wrong. Retention is measured from delivery, not from when the event happened, so a row delivered a minute ago is kept however old its event is. That is the behaviour you want, because it protects a consumer that has been down for a week rather than deleting its backlog for being old. I had written an assertion for a rule I had not thought through.

A failing assertion has two sides and only one of them is usually examined. This is the same instinct as reaching for a script before an agent: when something disagrees with you, find out which half is the authority before changing either.


If your team has a suite that passes and a system that does not, that’s the work I do.


Working through this in your own team?

I help engineering teams adopt AI coding assistants without giving up engineering rigour, and design the architecture underneath. If that's on your plate, let's talk.