« Phase 12 · Warmup · Track Overview

Lab 01 — The Integration Fabric

The problem

The agent has decided. Identity, policy and the action gateway have all said yes. Now the decision has to reach a mainframe.

Between here and there:

  • The payment must be expressed in ISO 20022, where a missing element is not a validation error but a rejected file, and where a JPY amount parsed with two decimal places is a payment a hundred times too large.
  • The rail you choose determines whether the payment can be recalled at all — and that determines whether a human had to approve it.
  • The database write and the event publish are two systems, and a crash between them leaves the bank and the platform disagreeing about what happened.
  • Downstream will receive the event more than once, because at-least-once is the only delivery guarantee anyone can actually offer.
  • Someone will add a field to the event schema, and whether that breaks production depends on which side deploys first.
  • And at the end of the day, two independent records have to agree — or somebody has to own the difference.

You build all of it.

What you build

#ComponentWhat it does
1MessageIdentifier, BUSINESS_AREASreading pain.001.001.09 — the map, not the details
2to_minor, from_minor, currency_exponentinteger minor units via Decimal, with the non-2-digit currencies
3valid_iban, valid_bicmod-97 and ISO 9362
4Pain001Parserfourteen rejection codes, every problem at once, each naming its element
5Rail, Finality, choose_railfinality as a scheme rule, and a rail choice that explains itself
6DualWriteStorethe problem, demonstrated — deliberately broken
7Outbox, OutboxRelaythe fix, with the at-least-once republish it honestly produces
8MessageLog, ConsumerGrouppartitions, offsets, lag, and where ordering holds
9IdempotentConsumerexactly-once effects under duplicate delivery
10check_backward/forward, SchemaRegistrycompatibility mode as deploy order
11DataProductContract, DataProductschema + quality + freshness, checked rather than asserted
12reconciletwo records, compared; a break is a finding

Key concepts

ConceptWhereWhy it matters
The message name is the mapMessageIdentifierpain = customer→bank, pacs = bank→bank, camt = statements
The namespace pins the versionPain001Parser.parsea .09 parser must not silently accept a .03 document
Integer minor unitsto_minorint(1.15 * 100) is 114, silently, on every payment
Decimal, never floatto_minorbinary floats cannot represent 0.1, and money is base 10
Not every currency has 2 digitsCURRENCY_EXPONENTJPY has 0, KWD has 3; assuming 2 is a 100× error
Refuse extra precisionto_minorrounding a payment amount silently is a daily reconciliation break
Every rejection at onceparseone error per round trip, in a bank, is one error per day
Rejections name the elementRejectionCdtTrfTxInf[1]/Amt versus "invalid message"
Cross-field checksCS01, CS02NbOfTxs and CtrlSum are what catch a truncated file
Duplicate EndToEndIdDU01downstream keys idempotency on it; a duplicate pays twice
Settlement dominates the cut-offfinality_atan instant payment is final at once, despite having no cut-off
Finality is a scheme ruleFinalityirreversible is not an opinion about difficulty
The rail choice explains itselfchoose_railan agent must be able to say why, because it drives approval
The dual-write problem has no ordering fixDualWriteStoreDB-then-publish loses; publish-then-DB invents
One transaction, two rowsOutbox.transactboth writes go to the same database, so atomicity covers them
The outbox is at-least-oncerun_onceit promises no lost events, not no duplicates
Derived partitioningpartition_forhash() is salted per process; ordering would break on restart
Ordering is per partitionMessageLogthere is no global order, and asking for one costs the parallelism
Commit after processingConsumerGroup.commitcommit first and a crash is at-most-once
The offset is the next onecommitoff-by-one means re-reading forever or skipping
seek is separate from commitseeka rewind must be deliberate
Exactly-once effectsIdempotentConsumerexactly-once delivery is two-generals-impossible
The dedup key is contentkey_ofan offset changes under a different partition assignment
Mode is deploy orderDEPLOY_ORDERBACKWARD → consumers first; getting it backwards is a release outage
New required fields need defaultscheck_backwardold data does not have them
A refused registration makes no versionregisterotherwise version numbers lie about what was live
Freshness makes it a productDataProductContracta promise with a number can be breached, so someone can be told
A break is a findingreconcileit runs daily and somebody is accountable; that is the whole value

Files

FileRole
lab.pyyour implementation
solution.pyreference; python solution.py runs a twelve-part worked session
test_lab.py144 tests
requirements.txtpytest

Run

pip install -r requirements.txt
pytest test_lab.py -v
LAB_MODULE=solution pytest test_lab.py -v
python solution.py

Success criteria

  • All 144 tests green against your lab.py.
  • to_minor("1.15", "USD") == 115 — and to_minor("100", "JPY") == 100.
  • Extra decimal precision is refused, not rounded.
  • An IBAN with one digit changed fails; a BIC with a digit in the institution code fails.
  • A pain.001 with six problems reports all six, sorted, each naming its element.
  • The wrong namespace is a rejection, not a silent parse.
  • NbOfTxs and CtrlSum disagreeing with the transactions are both caught.
  • A duplicate EndToEndId within one file is rejected.
  • An instant payment is FINAL immediately; a batch payment before cut-off is REVOCABLE.
  • choose_rail always returns a non-empty reason, including when it returns no rail.
  • A dual-write crash leaves the row and loses the event.
  • An outbox crash before commit leaves neither, and consumes no sequence number.
  • A relay crash after publishing republishes — at-least-once, honestly.
  • The same key always routes to the same partition, in this process and the next.
  • commit cannot rewind; seek can.
  • Replaying the whole log five times produces one effect per event.
  • A crash before commit re-delivers and does not re-effect.
  • Each compatibility mode refuses exactly the right changes, and states its deploy order.
  • A refused registration leaves the version count unchanged.
  • A product fails on a missing field, a wrong type, a failed rule, or a stale clock.
  • reconcile reports mismatches and both directions of absence, sorted by key.

How this maps to the real stack

This labThe real thingWhat we simplified
Pain001Parseran ISO 20022 library + the scheme's own usage guidelinesone message, a subset of elements; no XSD, no CBPR+/HVPS+ market practice
RAILSUAEFTS, IPI, WPS, SWIFT gpiinvented cut-offs; real ones vary by currency, day and correspondent
OutboxDebezium outbox pattern, or a relay reading a tableno database; no polling interval, no ordering guarantees across aggregates
MessageLogKafka, Azure Event Hubsin-memory; no replication, no rebalancing, no retention, no compaction
ConsumerGroupa Kafka consumer groupno rebalancing — which is where most real bugs live
IdempotentConsumera dedup table in Postgres/Redis with a TTLan unbounded in-memory set
SchemaRegistryConfluent Schema Registry, Azure Schema Registryno Avro/Protobuf, no serialization, no subject naming strategies
DataProducta Databricks/Cloudera data product with Great Expectations or Sodano storage, no lineage, no access control
reconcilea nightly recon job with a break-management workflowno tolerance rules, no ageing, no break assignment

Honest limits. The parser covers one message and a subset of its elements — real ISO 20022 validation is an XSD plus a market-practice guideline (CBPR+ for cross-border, HVPS+ for high value) that constrains what the schema permits, and the guideline is where most rejections come from. The consumer group has no rebalancing, which is the single richest source of real bugs: a partition moving between consumers mid-batch is how you get duplicate processing and lost commits, and it is exactly what makes the idempotent consumer non-optional. The dedup set is unbounded, so in production it needs a TTL and therefore a decision about how late a duplicate can arrive. The outbox has no polling interval, no batch size and no ordering guarantee across aggregates. And the rails are invented: real cut-offs vary by currency, by day, by correspondent bank, and by whether it is a Friday in the UAE.

Extensions

  1. Add rebalancing. Two consumers, one group, partitions reassigned mid-batch. Watch the duplicate processing appear, then confirm the idempotent consumer absorbs it. This is the exercise that makes the design click.
  2. Bound the dedup set. Add a TTL, then answer: how late can a duplicate arrive? The answer is the retention of your log, which makes it a capacity decision.
  3. Validate against the real XSD. Download the pain.001.001.09 schema and run xmlschema against your fixtures. Then read a CBPR+ guideline and count how many additional rules it imposes.
  4. Add pacs.002. The status report that comes back. Now model the full lifecycle: initiated → accepted → rejected → settled, and map each to a Finality.
  5. Compacted topics. Add log compaction keyed on the message key, and work out what it means for a consumer that joins late — it sees the latest state, not the history, which changes what your handler can assume.
  6. Transactional outbox with CDC. Replace the polling relay with a Debezium-shaped reader over a simulated transaction log, and handle the schema-drift problem it hands you.
  7. A data-product lineage graph. Record which products a product derives from, then answer "what breaks if agent.traces is late?" (Phase 15).
  8. Reconciliation with tolerance. Real recon allows a small tolerance and ages breaks. Add both, and discover that "how old is the oldest unresolved break" is the metric people actually watch.

Interview / resume bullets

  • "Built the platform's payment-initiation path against ISO 20022, with a validator that reports every rejection at once and names the offending element — turning a six-problem file from six days of round trips into one."
  • "Represented money as integer minor units with per-currency exponents, which removed an entire class of cross-border defect: JPY has no minor digits and KWD has three, and the two-decimal assumption is a hundred-fold error."
  • "Mapped scheme finality onto the platform's side-effect classes, so an agent's autonomy is bounded by the rail it selects — an instant payment requires an approval that a pre-cut-off batch payment does not."
  • "Eliminated the dual-write problem with a transactional outbox and idempotent consumers, giving exactly-once effects under at-least-once delivery without any exactly-once delivery claim."
  • "Made schema compatibility mode an explicit deploy-order decision rather than a release-day discovery, and enforced it in the registry so an incompatible schema cannot be published."
  • "Published the AI platform's traces, evaluations and outcomes as contract-bearing data products with freshness SLOs — applying to ourselves the discipline we asked of every upstream system."