« 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
- 2. The map
- 3. The vocabulary
- 4. The order of checks, and why
- 5. The side-effect table, memorized
- 6. The five things that will surprise you
- 7. Reading a resilience config
- 8. Where the neighbouring phases connect
- 9. What to build first
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
| Term | Means |
|---|---|
| Side-effect class | read / write_idempotent / write_non_idempotent / irreversible |
| Idempotency key | a caller-supplied token making a retry safe |
| Exactly-once effect | at-least-once delivery + idempotent handling; the achievable version |
| Saga | local transactions with compensations, in place of 2PC |
| Compensation | a new action undoing a business effect — not a rollback |
| Orphan | a compensation that itself failed; there is no third level of undo |
| Circuit breaker | closed / open / half-open, driven by a failure rate |
| Minimum throughput | the request floor below which the failure rate is noise |
| Bulkhead | a per-dependency concurrency limit |
| Dual control | four-eyes: two distinct authenticated humans |
| Maker-checker | the banking name for the same thing |
| Obligation | something policy requires the gateway to do on allow (Phase 09) |
| Hash chain | each record's hash covers the previous hash |
| Tamper-evident | edits are detectable. Not tamper-proof — that needs an external anchor |
| WORM | write-once-read-many storage |
4. The order of checks, and why
The sequence is not arbitrary; each position earns its place.
| # | Check | Why here |
|---|---|---|
| 1 | unknown tool | cheapest possible refusal |
| 2 | contract | purely local; no state touched |
| 3 | key required? | derived from the class; still local |
| 4 | dual control | before the key is reserved, so a rejected approval does not burn the key the caller will reuse |
| 5 | idempotency | reserves state; must come after everything that can refuse for free |
| 6 | breaker | immediately before execution, so an open circuit does not consume a key |
| 7 | execute | — |
| 8 | audit | every 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
| retry | key | dual control | compensable | example | |
|---|---|---|---|---|---|
read | 3× | no | never | n/a | payments.lookup |
write_idempotent | 3× | yes | never | yes | crm.append_note |
write_non_idempotent | 1 | yes | never | yes | case.create |
irreversible | 1 | yes | ≥ threshold | no | payments.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_BASEDoverCOUNT_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
| Phase | Gives this phase | Takes from this phase |
|---|---|---|
| 01 — Agent kernel | the proposal, and the pause state for HITL | the execution chain entry |
| 02 — MCP tool plane | the tool schema and the side-effect declaration | contract enforcement at call time |
| 08 — Identity | the JIT credential and the actor chain | the chain in the audit record |
| 09 — Control plane | the decision, the policy version, the obligations | the record that the decision was enforced |
| 11 — Guardrails | injection detection before the proposal is trusted | the enforcement point for a blocked action |
| 12 — Integration fabric | the downstream adapters, ISO 20022, the outbox | the transactional boundary |
| 14 — SRE | — | breaker state, retry rate, saga orphan count as SLIs |
| 15 — Governance | — | the evidence pack's primary artifact |
9. What to build first
- 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.
- 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.
- Idempotency, before the first write tool ships. Retrofitting it means auditing every existing call site for double-execution.
- Contract enforcement with invariants. The schema half is easy; insist on the second half from the start, or business rules migrate into prompts.
- The breaker and the bulkhead, when the first downstream has a bad day. Build the bulkhead even though the breaker is the famous one.
- 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.
- Sagas, only when a genuine multi-system flow appears — and reach for Temporal rather than building durability yourself.