Claim Check
Like a left-luggage office: drop the suitcase (the payload), get a ticket (the reference), and the ticket travels — not the suitcase. The pattern you cannot skip as soon as EDI messages weigh more than a few hundred kilobytes.
Problem
Messaging queues (RabbitMQ, SQS, Kafka, Service Bus) are sized for messages of a few kilobytes — not for 50 MB EDIFACT INVOIC files or 200 MB multi-delivery X12 DESADV. Putting these large payloads in the queue: (a) saturates broker bandwidth, (b) slows every other message, (c) hits native limits (SQS caps at 256 KB, Service Bus standard at 256 KB, EventBridge at 256 KB), (d) inflates queue storage cost. But we cannot do without the content either — we must keep and audit-close it.
Forces
- Cost asymmetry. An object store (S3, Azure Blob, GCS) costs ~10× less per gigabyte than a broker and holds terabytes effortlessly. A broker is not storage.
- Access asymmetry. The pipeline often only needs a subset of the payload (the header, a few fields) to route, validate, dedup. Loading 100 MB to read 200 bytes is wasteful.
- Long retention. EDI legal obligations (5-10 years in France for invoices) require long storage that a broker is not built for.
- Audit-closeness. The original payload must be archivable with hash, timestamp, sealing — all things object stores do natively.
Solution
EIP §358 (Hohpe & Woolf, 2003), as Store In Library or Claim Check. The producer deposits the full payload into an object store (with encryption at rest, SHA-256 computation, versioned archiving), then publishes a small message in the queue containing: envelope metadata, hash, size, and the payload URI. The consumer reads the small message; if it needs the payload, it GETs it from the object store. The message stays light to transit; the payload stays indexable and archivable.
Producer Consumer
──────── ────────
payload 100MB
│
▼
┌─────────────┐
│ store( │ ──▶ s3://archive/2026/05/14/inv-001.edi
│ payload) │ (full payload archived)
└─────────────┘
│
▼ ref = "s3://archive/2026/05/14/inv-001.edi"
┌─────────────────────────────────────────────┐
│ small message { +metadata, +ref } │ ──▶ queue
└─────────────────────────────────────────────┘
│
▼
consumer reads small msg
│
▼
if needs payload, fetch
s3://archive/... Topology
Two variants:
- Mandatory Claim Check. Every message goes through the object store, regardless of size. Simple, uniform.
- Threshold-based. Only messages above a threshold (say 64 KB) are externalised. Below the threshold, the payload stays inline. More efficient for small messages but harder to maintain.
EDI implementation
For a 100 MB EDIFACT INVOIC:
- The transport (AS2 / AS4 / SFTP) receives the payload, computes its SHA-256 hash.
-
The payload is stored in the object store under a canonical key
s3://ediverse-archive/<year>/<month>/<day>/<interchangeRef>.edi. - The following small message is published to the queue:
{
"claimCheckVersion": "1",
"type": "INVOIC",
"interchangeRef": "INV202605140001",
"from": "BUYER_GLN",
"to": "SUPPLIER_GLN",
"sizeBytes": 105374208,
"sha256": "9a4b1c2def8b6a5e8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b",
"claimCheck": {
"uri": "s3://ediverse-archive/2026/05/14/inv-202605140001.edi",
"expiresAt": "2031-05-14T00:00:00Z",
"encryption": "AES256",
"contentType": "application/edifact"
}
}
The consumer (Normalizer, validator) reads this message. For most
upstream operations it does not even need the full payload — the
header, the routing metadata and the hash are enough. When it
needs the payload (parse, translation), it does GET s3://... with a range if possible (partial header
read for the sniffer, then full read for translation).
Blob lifecycle
Three questions to settle:
- Retention duration. Legal obligations vary by message type: 5 to 10 years for invoices (Article L102B of the French General Tax Code in France, 6 years US IRS). ORDERS and DESADV have shorter requirements (3-5 years). Configure an S3 lifecycle rule per prefix.
- Storage tier. Beyond 90 days, switch to cold storage (S3 Glacier, Azure Cool Blob). Cost × 10 lower, access latency in hours — acceptable for occasional audit.
- Encryption and sealing. Enable at-rest encryption by default (SSE-KMS). For financial flows, add a detached timestamped signature (CMS / PKCS#7) to make the blob legally unassailable.
Anti-patterns
- Early blob deletion. If blob retention is shorter than the possible investigation window, traceability is lost. Blob retention must be ≥ the retention of every reference that points to it.
- No hash. Without a hash in the small message, we cannot verify the downloaded blob matches what the producer announced. Undetected tampering risk.
- Protocol-coupled URI. A
file:///mnt/edi/...URI is not portable to the cloud. Prefer an abstract URI the runtime translates to concrete access (factory pattern). - No orphan handling. If the pipeline crashes between blob upload and message publish, we have an orphan blob. Plan periodic cleanup of blobs without a known reference (with safety: mark for deletion, wait 30 days, delete).
- Claim check for everything. For a 500-byte CONTRL, externalising to S3 adds latency with no benefit. Keep the threshold-based option.
Related patterns
- Dead Letter Channel — uses the claim check for the payload archived in the DLQ.
- Normalizer — reads the payload via the claim check for translation.
- Aggregator — stores children via claim check to avoid saturating memory during the window.
Sources
- Hohpe G., Woolf B. — Enterprise Integration Patterns, Store In Library / Claim Check (§358). enterpriseintegrationpatterns.com — Store In Library
- AWS Architecture Blog — Implementing the Claim Check pattern using AWS, 2021. Modern practice with S3 + SQS / EventBridge. aws.amazon.com/blogs/compute
- Azure Architecture Center — Claim-Check pattern. Official Microsoft reference. learn.microsoft.com — Claim-Check
- French General Tax Code, Article L102B. 10-year retention of electronic invoices, legal foundation of the storage lifecycle.