« Phase 10 · Warmup · Track Overview

Hitchhiker's Guide — The Action Gateway

The fast orientation. What the pieces are, what they are called, and how they fit — before the deep dive takes them apart.


Table of Contents


1. Don't panic: the one-paragraph version

The action gateway is the only thing in the platform that talks to the bank. An agent produces a proposal — a tool name and some arguments — and the gateway decides whether that proposal becomes an effect. It checks the proposal against a declared contract, derives its retry and approval policy from the tool's declared side-effect class, refuses to execute twice for the same idempotency key, requires two humans above a value threshold, protects the downstream with a circuit breaker, runs multi-step work as a saga with compensations, and writes a hash-chained record of everything — including the refusals.

2. The map

   agent proposes
        │  {tool: "payments.release", args: {...}}
        ▼
   ┌────────────────────────── ACTION GATEWAY ───────────────────────────┐
   │                                                                     │
   │  1. contract      schema  +  business invariants                    │
   │  2. key present?  derived from the side-effect class                │
   │  3. dual control  two distinct humans, requester excluded           │
   │  4. idempotency   fresh / replay / conflict / in-flight             │
   │  5. breaker       closed? half-open probe? open -> fallback         │
   │  6. execute       attempts = policy.max_attempts                    │
   │  7. audit         hash-chained, redacted, every path                │
   │                                                                     │
   └──────────────────────────────┬──────────────────────────────────────┘
                                  │ JIT credential (Phase 08)
                                  ▼
                    core banking · payments · CRM · treasury

3. The vocabulary

TermMeans
Side-effect classread / write_idempotent / write_non_idempotent / irreversible
Idempotency keya caller-supplied token making a retry safe
Exactly-once effectat-least-once delivery + idempotent handling; the achievable version
Sagalocal transactions with compensations, in place of 2PC
Compensationa new action undoing a business effect — not a rollback
Orphana compensation that itself failed; there is no third level of undo
Circuit breakerclosed / open / half-open, driven by a failure rate
Minimum throughputthe request floor below which the failure rate is noise
Bulkheada per-dependency concurrency limit
Dual controlfour-eyes: two distinct authenticated humans
Maker-checkerthe banking name for the same thing
Obligationsomething policy requires the gateway to do on allow (Phase 09)
Hash chaineach record's hash covers the previous hash
Tamper-evidentedits are detectable. Not tamper-proof — that needs an external anchor
WORMwrite-once-read-many storage

4. The order of checks, and why

The sequence is not arbitrary; each position earns its place.

#CheckWhy here
1unknown toolcheapest possible refusal
2contractpurely local; no state touched
3key required?derived from the class; still local
4dual controlbefore the key is reserved, so a rejected approval does not burn the key the caller will reuse
5idempotencyreserves state; must come after everything that can refuse for free
6breakerimmediately before execution, so an open circuit does not consume a key
7execute
8auditevery path, including all six refusals above

The two orderings that are genuinely load-bearing are 4-before-5 and 6-after-5. Both are the same insight in different clothes: the idempotency key is a scarce resource, and burning one on a refusal makes the caller's retry fail for the wrong reason.

5. The side-effect table, memorized

retrykeydual controlcompensableexample
readnonevern/apayments.lookup
write_idempotentyesneveryescrm.append_note
write_non_idempotent1yesneveryescase.create
irreversible1yes≥ thresholdnopayments.release

Two questions this table answers instantly in an interview:

"Why isn't irreversible just write_non_idempotent?" — because for a non-idempotent write, the key makes a retry safe; for an irreversible action, "did it happen?" may have no answer inside the retry window, so the gateway does not guess and a human decides.

"What's the default?" — there isn't one. A tool that has not declared cannot be registered.

6. The five things that will surprise you

1. A different request on the same key executes never. The instinct is to just run it. Don't: the caller's key generation is broken, so their model of what they have already done is broken too.

2. Compensation is visible. You will design as if it were a rollback. It is not — the hold was seen, the reversal is its own line on the statement.

3. The breaker's minimum throughput matters more than its threshold. Without it the breaker opens at 3 a.m. on one failure and gets tuned off within a month.

4. Refusals go in the audit log. They are the more interesting half. "The platform stopped it" is what proves the control exists.

5. The audit write is not best-effort. If it cannot be written, the action does not proceed. That is a real availability cost, and it is the right trade in a bank.

7. Reading a resilience config

resilience4j, which is the shape most of these take:

resilience4j.circuitbreaker:
  instances:
    coreBanking:
      slidingWindowType: TIME_BASED          # not COUNT_BASED — see below
      slidingWindowSize: 60                  # seconds
      minimumNumberOfCalls: 10               # ← the parameter people omit
      failureRateThreshold: 50               # percent
      slowCallRateThreshold: 50              # slow counts as failure...
      slowCallDurationThreshold: 2s          # ...past this
      waitDurationInOpenState: 30s
      permittedNumberOfCallsInHalfOpenState: 3
      automaticTransitionFromOpenToHalfOpenEnabled: true

resilience4j.bulkhead:
  instances:
    coreBanking:
      maxConcurrentCalls: 25                 # the control the breaker doesn't give you
      maxWaitDuration: 0                     # reject immediately, never queue

Three things to notice:

  • TIME_BASED over COUNT_BASED. A count-based window on a low-traffic endpoint can span hours, so the breaker reacts to failures that are long over.
  • slowCallRateThreshold. This is how resilience4j folds the slow case into the breaker. It is not a substitute for a bulkhead, but it is better than nothing.
  • maxWaitDuration: 0. A bulkhead that queues is a bulkhead that has moved the problem.

8. Where the neighbouring phases connect

PhaseGives this phaseTakes from this phase
01 — Agent kernelthe proposal, and the pause state for HITLthe execution chain entry
02 — MCP tool planethe tool schema and the side-effect declarationcontract enforcement at call time
08 — Identitythe JIT credential and the actor chainthe chain in the audit record
09 — Control planethe decision, the policy version, the obligationsthe record that the decision was enforced
11 — Guardrailsinjection detection before the proposal is trustedthe enforcement point for a blocked action
12 — Integration fabricthe downstream adapters, ISO 20022, the outboxthe transactional boundary
14 — SREbreaker state, retry rate, saga orphan count as SLIs
15 — Governancethe evidence pack's primary artifact

9. What to build first

  1. The side-effect class on every tool, with no default and a registration that refuses without it. Ten minutes, and it is the field everything else derives from.
  2. The audit record shape, including the actor chain and both versions. Retrofitting a field into an audit log means the first six months of records lack it.
  3. Idempotency, before the first write tool ships. Retrofitting it means auditing every existing call site for double-execution.
  4. Contract enforcement with invariants. The schema half is easy; insist on the second half from the start, or business rules migrate into prompts.
  5. The breaker and the bulkhead, when the first downstream has a bad day. Build the bulkhead even though the breaker is the famous one.
  6. Hash chaining, before the log is something an examiner will read. Chaining a log after the fact leaves an unchained prefix that is exactly as trustworthy as it was.
  7. Sagas, only when a genuine multi-system flow appears — and reach for Temporal rather than building durability yourself.