R26 — Cutoffs, kick-off and warehousing: scheduled state transitions and deferred work

This is the part of payments that has no analogue in a normal web system, and it is where domain candidates separate themselves. A payment engine does not run on wall-clock time; it runs on a business date per rail, opened and closed by scheduled processes, with instructions parked until their moment arrives.

One business day on a rail
flowchart LR
  KO["Kick-off<br/>roll business date, open session,<br/>refresh FX and limits"] --> REL["Release warehoused payments<br/>whose value date is now"] --> REV["Revalidate each one<br/>account, sanctions, mandate, limits"] --> OPEN["Session open — accept and submit"]
  OPEN --> SC["Soft cutoff<br/>stop accepting, prioritise high value"] --> DR["Drain buffer<br/>submit worst-case backlog + one retry"] --> HC["Hard cutoff — rail enforces"] --> EOD["End-of-day: final batch, acks,<br/>statement to reconciliation"] --> CLS["Date closed, then rolled"]
  SC -.->|"missed the window"| NEXT["Rolls to next window<br/>value date may change — tell the customer"]
Warehouse states and the four dates
flowchart TD
  CAP["Captured<br/>captured_at = UTC audit fact"] --> W["warehoused<br/>value_date in the future<br/>or release_after set"]
  W -->|"cancel or amend<br/>races the release job"| CAN["cancelled"]
  W -->|"kick-off on value date"| RV["revalidate at release"]
  RV -->|"fails: sanctioned, closed, expired"| PARK["parked for investigation"]
  RV -->|passes| REL["released<br/>business_date assigned"]
  REL --> SUB["submitted"] --> ACK["acknowledged<br/>rail decides the business date"] --> SET["settled<br/>settlement_date known only now"]
  W -.->|"past value date and still here"| ALERT["ALERT — independent of the job's health check"]

The four dates, kept distinct

Confusing these is the most common modelling error in payment systems.

Concept Meaning
Capture timestamp When the instruction physically arrived, in UTC. An audit fact, never a business decision input
Business date The rail's accounting day, opened at kick-off and closed at cutoff. Not midnight, and not your server's date
Value date The date the customer wants funds to move — may be today or months out
Settlement date When money actually settled, known only afterwards; may differ from value date after a return or a missed cutoff

Store every timestamp in UTC with the originating zone alongside it, and store business date as a plain date with a rail attached. A payment submitted at 23:50 IST may belong to tomorrow's business date on a US rail and today's on a domestic one, simultaneously.

Cutoffs and clearing windows

Each rail has one or more daily windows with a hard cutoff, expressed in the rail's local time and therefore moving twice a year with daylight saving. ACH has several same-day windows and a standard window; high-value rails (RTGS) have a single long session with a late cutoff for bank-to-bank traffic; instant rails run continuously but still roll a business date for accounting.

Operationally you need three times per window, not one: the soft cutoff at which you stop accepting new instructions for that window, the hard cutoff the rail enforces, and the buffer between them, which exists so you can drain the queue. Sizing that buffer is a real engineering decision — it must exceed the time to submit the worst-case backlog plus one retry cycle.

As the soft cutoff approaches, behaviour changes: prioritise high-value and time-critical payments, stop accepting low-priority batch work, and warn callers that new instructions will land in the next window. Missing a cutoff is not an error to retry blindly; the payment rolls to the next window, the value date may change, and the customer must be told.

Kick-off and end-of-day

Kick-off (start-of-day) opens a new business date for a rail. It typically: rolls the business date forward using the rail's holiday calendar, opens the clearing session, releases warehoused payments whose value date is now current, refreshes FX rates and limits, and re-runs validation on everything it releases.

End-of-day closes the window: stop accepting, drain, submit the final batch, receive acknowledgements, generate the statement and hand over to reconciliation, then mark the date closed.

Both are jobs that will fail at some point, so design them accordingly: idempotent (running kick-off twice must not release a payment twice), resumable from a step marker, observable (a kick-off that did not run by its scheduled time is a page, not a dashboard item), and with a manual override, because at some point an operator will need to open a session by hand.

Date rollover

Rollover is where bugs live. Handle these explicitly:

Warehousing: holding payments until their moment

Date warehousing is holding a future-dated instruction until its value date. A corporate submits payroll on the 25th for value date the 30th; the instruction is accepted, validated and stored, then released at kick-off on the 30th. Warehouses routinely hold instructions for months.

Time warehousing is the intraday version: holding until a specific window or time — until the next same-day ACH window, until funding arrives, until an FX rate is fixed, until a limit frees up.

The critical design rule: validate at intake, revalidate at release. Between the two, the account may have closed, the beneficiary may have been sanctioned, the mandate may have expired, limits may have changed and the FX rate certainly has. A system that trusts its intake-time checks will eventually release a payment to a sanctioned party, which is a regulatory incident rather than a bug.

CREATE TABLE payment_instruction (
  id              bigserial PRIMARY KEY,
  captured_at     timestamptz NOT NULL,   -- UTC audit fact
  rail            text NOT NULL,
  value_date      date NOT NULL,
  release_after   timestamptz,            -- time warehousing
  business_date   date,                   -- assigned at release
  state           text NOT NULL,          -- warehoused | released | submitted
                                          -- | acknowledged | settled | returned
  priority        int NOT NULL DEFAULT 5,
  revalidated_at  timestamptz
);
CREATE INDEX ON payment_instruction (rail, value_date)
  WHERE state = 'warehoused';

CREATE TABLE rail_session (
  rail          text NOT NULL,
  business_date date NOT NULL,
  window_name   text NOT NULL,
  soft_cutoff   timestamptz NOT NULL,
  hard_cutoff   timestamptz NOT NULL,
  state         text NOT NULL,   -- pending | open | draining | closed | rolled
  PRIMARY KEY (rail, business_date, window_name)
);

The release job selects warehoused instructions for the rail and date, revalidates, assigns the business date, and transitions state — all keyed so a rerun is a no-op. Release in a deterministic order (priority, then capture order) so behaviour is reproducible, and watch for the thundering herd: a month-end kick-off may release hundreds of thousands of payroll payments at once, which is a capacity event you plan for rather than discover.

Amendment and cancellation. A warehoused payment can be cancelled or amended right up to release — a whole API surface that only exists because of warehousing, with its own race against the release job. Take the row lock in the release job and let cancellation lose or win cleanly; never both.

What interviewers probe here

What happens if kick-off does not run — do payments silently miss their value date, and would you know? Answer: a payment past its value date and still warehoused is an alert, independent of the job's own health check. What happens to a payment submitted seconds before cutoff and acknowledged after? What if the rail's file arrives late and the window cannot close? How do you handle a holiday calendar change announced mid-month? And the operational one: who can open a session manually at 2am, and what stops that from becoming routine?

The framing to carry through all of it — in payments, time is business state, not infrastructure. It is modelled, persisted, audited and alerted on, exactly like money.

What to say in the interview

"In payments, time is business state rather than infrastructure. I keep four dates distinct — capture timestamp, business date, value date, settlement date — and derive the business date from one clock service rather than from each container's idea of today. Warehoused instructions are validated at intake and revalidated at release, because an account can close or a beneficiary can be sanctioned in between. Kick-off and end-of-day are idempotent, resumable jobs with their own alerting: a payment past its value date and still warehoused pages someone, independent of whether the job reported success."