ediverse Explore the platform

Spotlight PEPPOL BIS Billing 3.0 The EU e-invoicing mandate is here — France Sept 2026, Belgium Jan 2026, Germany 2025.

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.

plaintext topology.txt
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:

  1. The transport (AS2 / AS4 / SFTP) receives the payload, computes its SHA-256 hash.
  2. The payload is stored in the object store under a canonical key s3://ediverse-archive/<year>/<month>/<day>/<interchangeRef>.edi.
  3. The following small message is published to the queue:
json claim-check-msg.json
{
  "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.
  • 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