How it is built
Architecture overview
A customs single window is the system through which a trader declares a shipment once and the state does the rest: classify the goods, work out the duty and taxes, decide how closely to check the shipment, ask the other agencies (health, agriculture, standards, transport) for their permits, and release the goods. The WTO Trade Facilitation Agreement describes the goal: traders submit documents and data through a single entry point, and agencies do not ask again for what they already received (Article 10.4, see Sources). Real systems are ASYCUDA, CBP's ACE, Singapore's TradeNet and the EU's customs systems; most are proprietary or government-built.
Three ideas hold the design together. First, a declaration is a document that is processed in stages, not a request that returns an answer: it is received, validated, rated, risk-scored, routed, checked, possibly corrected, and finally cleared, and each stage can pause for days. Second, the rules are data with validity dates: tariffs, origin rules, bans and quotas change on known dates, and the system must answer "what applied to this goods code on the day of import". Third, risk, not rules, decides who is checked: a small share of declarations gets a document check or an inspection; the rest are released automatically, and the scoring that decides which is the lever customs administrations tune most.
Parts:
- Declaration intake — receive, authenticate and validate trader and broker submissions — Mutual TLS, XML streaming parser, x12-edi-messaging (no page yet), Idempotency keys, Authorization models
- Master data — parties, goods codes, countries, currencies, rules; versioned over time — Entity resolution, record-linkage (no page yet), bitemporal-data (no page yet), Interval tree, Time partitioning, MVCC, fuzzy-string-matching (no page yet), point-in-time-join (no page yet)
- Classification and tariff — goods code to duty, taxes, measures and prohibitions — Aho-Corasick, Text classification, Rete rules engine, tariff-rating (no page yet), Money representation, largest-remainder-rounding (no page yet)
- Valuation and origin — customs value and country-of-origin determination — Rete rules engine, currency-cross-rate-triangulation (no page yet), Topological sort, bill-of-materials-explosion (no page yet)
- Risk management — score every declaration, choose a lane, feed back audit results — Gradient-boosted trees, Calibration, Reputation scoring, Bayesian rate estimation, Sliding-window counters
- Workflow and multi-agency window — one case moving through many agencies' steps — Durable workflow execution, Saga pattern, Hierarchical state machine, Hierarchical timing wheel, Durable job queue, Transactional outbox, publish-subscribe-event-bus (no page yet)
- Accounts and payment — duties as receivables, guarantees, refunds — Double-entry ledger, Payment state machine, Reconciliation
- Records, audit and reporting — signed history, post-clearance audit, statistics — Tamper-evident audit log, Change data capture, Point-in-time recovery, Row-level security, trusted-timestamping (no page yet)
- Availability and assurance — keeps the window open at filing deadlines, measured and tested — Hot-standby redundancy, Primary-backup replication, Process supervision, Graceful draining, hdr-histogram (no page yet), Deterministic simulation testing, Coverage-guided fuzzing
trader / broker ──declaration (WCO-style message)──► intake: mTLS, schema check, idempotency
│
master data (versioned) ◄── tariff & rules updates
▼
classify ► rate duty ► value + origin ► permits requested from agencies
│
risk engine: score ► lane (release | docs | inspect)
│
┌───────────── workflow case (days, many actors) ──────────────┐
│ agency A permit agency B permit inspection payment guarantee │
└─────────────────────────────┬─────────────────────────────────┘
▼
release ► ledger posting ► audit log ► change feed ► statistics, post-clearance audit
How the parts fit together
Declaration intake
Declarations arrive from traders, brokers and carriers as structured messages. Many administrations model them on the WCO data model; HMRC's public Customs Declarations service, for example, validates each request against the WCO declaration schema before passing it on (see its repository). Intake does three things and nothing else: authenticate the sender (Mutual TLS for machine-to-machine links, Authorization models for what a broker may declare for which company), check the message against the schema, and store it with an idempotency key so a retry after a timeout does not create a second declaration. Large messages are read with an XML streaming parser in constant memory; legacy partners still send EDIFACT-style interchanges (x12-edi-messaging, no page yet). The response is only an acknowledgement; results arrive later as notifications, as the HMRC description also says.
Master data
Traders, importers, consignors, carriers and goods are referenced by id. The same company appears under many spellings and registrations, so Entity resolution (blocking, scoring, clustering; record-linkage, no page yet) links party records for risk history. The key property is time: a tariff measure has a validity range, a company's authorisation has a validity range, a ban has a validity range, and corrections can be retroactive. bitemporal-data (no page yet) stores both the real-world validity and the time the system learned it, so the system can answer "what did we believe on the day we cleared this?". Range lookups ("which measures cover this code on this date") use an Interval tree or sorted range arrays; old months are cut with Time partitioning; MVCC lets a declaration be rated against a stable snapshot while a rule update is loaded.
fuzzy-string-matching (no page yet) is the candidate step in front of entity resolution: it finds party and address records whose names differ by a typo, an abbreviation or a transliteration, so blocking does not miss a trader who declared under a variant spelling. point-in-time-join (no page yet) is how declarations meet the versioned reference data: each declaration is joined to the tariff measure, authorisation or ban that was valid on its own relevant date, not the one valid today, which is what makes a retroactive correction or a late declaration come out right.
Classification and tariff
Every line of a declaration carries a goods code. The international Harmonized System gives six digits that are used worldwide; each administration adds its own further digits, and decisions on specific products are national (GOV.UK). The tariff is a tree over those digits, and measures (duty rate, quota, licence, prohibition, certificate) attach at any depth and are inherited downward, filtered by origin country, date and trade agreement. The UK Trade Tariff backend is the clearest open example of this data model: it imports tariff updates and serves commodity codes, duties, measures, quotas, certificates and rules of origin (see trade-tariff/trade-tariff-backend).
Goods descriptions in the declaration are free text, so classification support is a Text classification problem (regex and keyword rules first, linear models or fine-tuned encoders next) with Aho-Corasick for fast keyword and prohibited-term matching. The classifier advises: it suggests a code or flags a mismatch between description and code, and the case goes to an officer when confidence is low. A binding decision comes from an advance ruling, which the WTO Agreement makes binding on the issuing authority for the applicant (Article 3). The duty calculation itself is Money representation work: integer minor units, a fixed rounding rule per line or per declaration, and exact allocation of lump-sum charges across lines (largest-remainder-rounding, no page yet). Rate-by-tier and per-unit rules are the tariff-rating (no page yet) pattern.
Valuation and origin
The customs value starts from the transaction value, the price actually paid, plus defined adjustments; if that cannot be used, the WTO Customs Valuation Agreement gives a fixed sequence of fall-backs: identical goods, similar goods, deductive value, computed value, and last a flexible method (see the agreement). That sequence is a decision list, easy to encode in a Rete rules engine where each rule states its conditions and a priority; exchange rates come from the administration's published rate (currency-cross-rate-triangulation, no page yet).
Origin decides whether a preferential rate applies. The WTO Agreement on Rules of Origin names the usual tests: goods wholly obtained in a country, or substantially transformed there, with "change in tariff classification" as one criterion (text). Proof is a bill-of-materials problem: a product's origin depends on the origin and classification of its inputs, so checking a claim means exploding the BOM (bill-of-materials-explosion, no page yet) in dependency order (Topological sort) and testing each rule. Most administrations only verify these claims after clearance, from the exporter's records.
Risk management
Article 7.4 of the Trade Facilitation Agreement asks members to adopt a risk management system, concentrate control on high-risk consignments, and expedite low-risk ones; selectivity criteria may include the HS code, description, country of origin, value, and the trader's compliance record. The engine has three layers:
- Rules from intelligence (a code and origin combination under suspicion, a sanctioned party), run in a Rete rules engine so that thousands of rules cost little per declaration.
- A learned score from Gradient-boosted trees trained on past inspection results (did the check find a fault?). Calibration makes the score a probability, so the inspection threshold can be set from capacity: "inspect the top few percent".
- Trader trust: a running record of how often a party's declarations were found compliant, kept with Reputation scoring and Bayesian rate estimation so that a trader with few declarations is not trusted too early, and Sliding-window counters for recent volumes. The Agreement's authorized-operator measures (Article 7.7) are the formal side of this: operators with a record of compliance get faster release.
The result is a lane: automatic release, document check, or physical inspection, with a random selection that keeps the model honest (Article 7.4 allows random selection too). Without random checks the model never learns about the low-scored fraud it misses; training data would be only what was already inspected.
Workflow and multi-agency window
The single window is mostly workflow. One declaration can wait for a veterinary certificate, a standards approval, an inspection slot, a payment and a guarantee, each owned by a different agency with its own system. Durable workflow execution fits: the case logic is ordinary code whose steps are logged, so it resumes after a crash while waiting for days. Calls to agencies are asynchronous: requests go out through a Transactional outbox so that "state changed" and "message to agency queued" commit together, and delivery uses a Durable job queue with retries. A cancelled declaration or a refused permit must undo earlier steps (release a guarantee, withdraw an agency request), which is the Saga pattern. Case states (received, held, inspection ordered, released, amended) form a Hierarchical state machine; deadlines (permit validity, storage time, appeal windows) are timers in a Hierarchical timing wheel.
The Agreement also asks for release before the final determination of duties if a guarantee is given (Article 7.3). In the data model this means a declaration has two lifecycles: physical release and financial finalisation, which can be weeks apart.
Agencies need to learn that a case changed without the window calling each of them in turn. publish-subscribe-event-bus (no page yet) carries the case events (submitted, held, inspected, released) on topics, and each agency subscribes to the topics its permits cover. The workflow publishes through the outbox, subscribers receive at least once and must tolerate repeats, and an agency's reply returns to the case as an event, so the case logic stays the single owner of the declaration's state.
Accounts and payment
Duties are money owed to the state. A Double-entry ledger records the liability at release, payments, guarantees drawn, and refunds as postings that sum to zero; the Payment state machine handles bank and card payments that time out into an unknown state; Reconciliation matches bank statements to the open items each day. Corrections after release (amendments, audit findings) are new postings, never edits.
Records, audit and reporting
Customs decisions are legally contestable, so the record must show who changed what and when. A Tamper-evident audit log holds decisions, officer actions and rule versions used. Post-clearance audit selects traders on risk and feeds its findings back into the score (Article 7.5.4). Change data capture streams committed changes to the statistics system and to partner agencies without double-writing, and Point-in-time recovery plus Row-level security (one trader sees only its own declarations) complete the data tier.
trusted-timestamping (no page yet) anchors the audit log's signed head with a time from an independent authority, so a decision's date can be proven in a dispute without trusting the customs system's own clock. It is applied to the log's periodic roots rather than to every entry, and the receipt is stored with the root.
Availability and assurance
Traders file against cut-off times, so the window cannot be down while a node is replaced. Primary-backup replication keeps a second copy of the declaration and case store, and Hot-standby redundancy keeps the intake and case services warm on a second node so a takeover is a switch, not a restore; Point-in-time recovery stays the answer for a bad write that replication would copy. Process supervision restarts a failed service, and Graceful draining lets it finish open submissions before a release. hdr-histogram (no page yet) records submission-to-lane latency, so the 99th and 99.9th percentiles can be watched in the hours before a filing deadline. Deterministic simulation testing replays failover, duplicate submissions and agency timeouts with injected faults and checks that no declaration is lost or released twice. Coverage-guided fuzzing mutates submitted XML and EDI messages, the input that arrives from outside parties.
Data and control flow
One import declaration
- The broker submits the message; intake authenticates, validates the schema, stores it under an idempotency key and acknowledges.
- The system resolves parties and loads the tariff as of the declaration date from the versioned rule store.
- For each line: check the goods code against the description, apply the measures (duty, quota, licence), compute the value and origin outcome, and compute duty in integer money.
- The risk engine scores the declaration and picks a lane.
- Required agency permits are requested; the workflow waits for each answer (minutes to days).
- If a check is ordered, an officer gets the case and records the result; findings update the trader record.
- On payment or a guarantee, the declaration is released; the ledger posts the liability; the audit log and change feed receive the events.
- Later: post-clearance audit samples declarations by risk and may issue an amendment.
Most time is spent waiting for people and agencies, not in computing: the calculation steps are milliseconds to seconds, while a held case takes hours or days. For this reason release-time measurement matters; the Agreement encourages members to measure and publish average release times (Article 7.6).
One tariff update
- A legal change arrives with a validity date.
- It is loaded as new versions with the valid-from date; nothing is overwritten.
- A diff against the current snapshot is reviewed, then published.
- Declarations lodged before the date continue to rate under the old rule, as their lodged date picks the version.
Scaling
- Case count is bounded by the workflow engine, not by CPU. Levers: keep case state small, partition by declaration id, archive finished cases to cheap storage.
- Spikes come around deadlines, month ends and port arrivals. Levers: accept and queue at intake (the acknowledgement is cheap), process asynchronously, drain with a bounded worker pool.
- Rules and tariff lookups become the hot path. Levers: cache the tariff snapshot per day in memory, precompute inherited measures per code, index measures by (code, origin, date).
- Risk scoring stays cheap if features are precomputed: trader history and code statistics are updated incrementally.
- Agency integration is limited by the slowest agency. Levers: timeouts with reminders, a dead-letter lane for stuck requests, a published service level.
- Availability matters near ports: a stopped system means trucks queue. Levers: replicas, a documented manual fallback procedure, and offline acceptance.
Failure modes
| Failure | Symptom | Cause | Fix |
|---|---|---|---|
| Wrong tariff version | duty differs from the legal amount | rule lookup by processing date instead of the legal date | key rules by declaration or acceptance date, bitemporal store |
| Duplicate declaration | goods declared twice, double liability | retry after timeout without idempotency | idempotency key at intake, duplicate check on trader plus reference |
| Misclassification | disputed duties, audit findings | free-text description, similar codes | classifier plus officer review, advance rulings, post-clearance audit |
| Risk model blind spot | fraud pattern never inspected | training only on inspected cases | random selection, periodic retraining on audit results |
| Over-selection | long queues, slow release | threshold too low, stale rules | set threshold from inspection capacity, retire rules that never hit |
| Stuck case | goods wait with no owner | agency answer never arrives, lost message | timers and reminders, dead-letter lane, outbox with retries |
| Rounding differences | totals do not match bank payment | per-line versus per-declaration rounding, floats | integer minor units, one named rounding rule |
| Retroactive correction breaks reports | past statistics change | update in place | append corrections with valid time, as-of reports |
| Trader sees another's data | leak between companies | filter in application code only | row-level security by trader id |
Real implementations
| name | what it is | repo | licence | notes |
|---|---|---|---|---|
| ASYCUDA World | UNCTAD customs management system used by many countries | — | — | proprietary programme of UNCTAD; see asycuda.org |
| CBP ACE | United States single window for import and export data | — | — | government system; see cbp.gov/trade/automated |
| TradeNet | Singapore's trade declaration single window | — | — | see tradenet.gov.sg |
| UK Trade Tariff backend | open tariff data model and API (codes, duties, measures, quotas, rules of origin) | trade-tariff/trade-tariff-backend | MIT | the tariff and measures part |
| HMRC Customs Declarations | API that validates declarations against the WCO schema and forwards them to the customs system | hmrc/customs-declarations | Apache-2.0 | the intake part; the backend system itself is closed |
Sources
- WTO, Agreement on Trade Facilitation: Article 3 (advance rulings), Article 7 (pre-arrival processing, separation of release, risk management, post-clearance audit, release times, authorized operators), Article 10.4 (single window).
- WTO, Agreement on Implementation of Article VII of the GATT 1994 (Customs Valuation).
- WTO, Agreement on Rules of Origin.
- GOV.UK, Using the Trade Tariff tool to find a commodity code.
- Repositories above: trade-tariff-backend and hmrc/customs-declarations (README text read).