Case & Pallet Aggregation Logic: DSCSA-Compliant Hierarchy Building, Validation, and Python Automation
Case and pallet aggregation logic is the process that binds serialized saleable units into scannable shipping containers and records those relationships as verifiable EPCIS events — and it is one of the load-bearing workflows within Aggregation Hierarchy & Validation Workflows, the parent domain that governs how serialized identifiers are nested and validated under the Drug Supply Chain Security Act (DSCSA). The specific problem this page solves is deceptively narrow but operationally unforgiving: at packaging-line speed, a system must prove that every unit physically placed in a case is the exact unit recorded as that case’s child, that every case on a pallet is accounted for, and that the resulting parent-child tree can be inflated by a downstream trading partner without a single orphaned or duplicated serial. Get it right and a distributor scans one pallet Serial Shipping Container Code (SSCC) instead of a thousand units; get it wrong and the inference collapses, verification requests return INVALID, and product is quarantined at the dock.
AggregationEvent only when counts and uniqueness pass; a defective tree is quarantined without stopping the line.Foundational Concepts & Identifier Data Contracts
Aggregation is only as trustworthy as the identifiers it links. Three GS1 identifier types carry the entire hierarchy, and each is a strict data contract before it is a barcode.
- SGTIN (saleable unit): the serialized GTIN encoded on the unit DataMatrix using Application Identifiers
(01)for the 14-digit GTIN,(21)for the serial,(17)for theYYMMDDexpiry, and(10)for the lot. These identifier rules originate in GS1 Standards Implementation and must be validated — check digit included — before a unit is ever eligible to become a child. - SSCC (case and pallet): the 18-digit Serial Shipping Container Code carried in AI
(00). Both the case and the pallet draw their SSCCs from the same managed serial pool, and the pool must guarantee global uniqueness so that a case SSCC can never collide with a pallet SSCC across parallel lines. - GLN (location): the Global Location Number that stamps the
readPointandbizLocationof every event, resolving where in the facility the aggregation physically occurred.
The relationship itself is expressed as an EPCIS AggregationEvent. A pallet-build event names the pallet SSCC as parentID, lists the constituent case SSCCs in childEPCs, and carries action: ADD, bizStep: urn:epcglobal:cbv:bizstep:packing, and a UTC eventTime with an explicit eventTimeZoneOffset. Case-build events follow the identical structure one tier down, naming the case SSCC as parent and the unit SGTINs as children. Two rules make this a legally meaningful contract rather than a convenience: completeness (every physical child appears exactly once in childEPCs) and immutability (once emitted, an aggregation is only ever reversed by a compensating action: DELETE event, never by editing history). The disaggregation and rebuild path that enforces the immutability rule is owned by Decommission & Reaggregation Rules, and the bidirectional integrity of each link is the concern of Parent-Child Serial Mapping.
AggregationEvent: the pallet SSCC becomes the parentID, while case SSCCs and unit SGTINs populate childEPCs one tier apart.Architecture: Where Aggregation Sits in the Line
A production aggregation stage is a small, decoupled pipeline rather than a single script. At the edge, vision systems and PLC-integrated scanners read unit SGTINs as cases are packed and case SSCCs as pallets are built. A synchronous tree builder assembles the parent-child map in memory, holding a partial container open until its expected child count is reached. A validation gate then decides, in single-digit milliseconds, whether the completed tree is emitted as an AggregationEvent or diverted to a quarantine queue. Only after the gate passes does the event flow into the asynchronous transmission path that carries it to the enterprise (L4) repository and on to trading partners and verification services.
The critical architectural decision is the same one that governs the whole parent domain: the synchronous line-side path must never block on the network. Aggregation trees are built and validated locally, buffered at the edge, and published asynchronously, so that a slow repository or a partner outage degrades into a growing buffer rather than a stopped palletizer. That buffered stream is what the Serialization Data Ingestion & EPCIS Event Sync layer consumes downstream.
Step-by-Step Implementation
The following build proceeds in the order the DSCSA data contract demands: model the identifiers, accumulate the hierarchy, validate it, then emit an immutable event. All snippets are Python 3.10+ and use Pydantic v2.
1. Model the aggregation with typed identifier validation
Satisfies the GS1 requirement that GTIN/SSCC structure and check digits are correct before an identifier enters a hierarchy. A malformed SSCC must never reach the tree builder.
from pydantic import BaseModel, field_validator
def _gs1_check_digit(body: str) -> int:
total = sum(int(d) * (3 if i % 2 == 0 else 1)
for i, d in enumerate(reversed(body)))
return (10 - total % 10) % 10
class Container(BaseModel):
sscc: str # AI (00), 18 digits
expected_children: int
@field_validator("sscc")
@classmethod
def valid_sscc(cls, v: str) -> str:
if len(v) != 18 or not v.isdigit():
raise ValueError("SSCC must be 18 numeric digits")
if _gs1_check_digit(v[:-1]) != int(v[-1]):
raise ValueError("SSCC check digit failed")
return v
class Child(BaseModel):
epc: str # SGTIN (unit) or SSCC (case)
lot: str # AI (10)
2. Accumulate the parent-child map at line speed
Satisfies the completeness rule: a container stays open until its physical child count matches the configured pack size, and a duplicate scan is rejected on contact rather than silently overwriting a slot.
class AggregationBuffer:
"""Holds one open parent and enforces uniqueness + capacity."""
def __init__(self, parent: Container):
self.parent = parent
self._children: dict[str, Child] = {}
def add(self, child: Child) -> None:
if child.epc in self._children:
raise ValueError(f"duplicate child {child.epc}")
if len(self._children) >= self.parent.expected_children:
raise ValueError("container over-capacity")
self._children[child.epc] = child
@property
def complete(self) -> bool:
return len(self._children) == self.parent.expected_children
def children(self) -> list[Child]:
return list(self._children.values())
3. Validate the completed hierarchy against the manifest
Satisfies the DSCSA requirement that the recorded hierarchy match the physical build order. Counts, uniqueness, and lot homogeneity are all checked before an event is allowed to exist.
def validate_tree(buf: AggregationBuffer,
manifest: set[str]) -> list[str]:
errors: list[str] = []
epcs = {c.epc for c in buf.children()}
if not buf.complete:
errors.append(f"incomplete: {len(epcs)}/"
f"{buf.parent.expected_children}")
missing = manifest - epcs
extra = epcs - manifest
if missing:
errors.append(f"missing children: {sorted(missing)}")
if extra:
errors.append(f"unexpected children: {sorted(extra)}")
lots = {c.lot for c in buf.children()}
if len(lots) > 1: # cross-lot contamination
errors.append(f"mixed lots in one container: {sorted(lots)}")
return errors
4. Emit an immutable AggregationEvent
Satisfies EPCIS 2.0 and the immutability rule. The event carries action: ADD, a UTC eventTime, and a deterministic idempotency key so a retried transmission never creates a duplicate parent-child record.
import hashlib
from datetime import datetime, timezone
def build_aggregation_event(buf: AggregationBuffer,
read_point_gln: str) -> dict:
child_epcs = sorted(c.epc for c in buf.children())
now = datetime.now(timezone.utc)
idem = hashlib.sha256(
f"{buf.parent.sscc}:{','.join(child_epcs)}".encode()
).hexdigest()[:24]
return {
"type": "AggregationEvent",
"eventTime": now.isoformat(),
"eventTimeZoneOffset": "+00:00",
"action": "ADD",
"bizStep": "urn:epcglobal:cbv:bizstep:packing",
"parentID": f"urn:epc:id:sscc:{buf.parent.sscc}",
"childEPCs": child_epcs,
"readPoint": {"id": f"urn:epc:id:sgln:{read_point_gln}"},
"idempotencyKey": idem,
}
For the fully asynchronous, broker-backed version of this pipeline — including the asyncio ingestion loop and EPCIS JSON-LD serialization — see Automating case-to-pallet aggregation validation in Python.
Validation & Error Handling
The validation gate has one job: never let a defective hierarchy become a transmitted event, and never let a single bad container stop the line. Failures fall into three classes, each with a distinct disposition:
- Structural defects (bad check digit, malformed SGTIN, wrong length SSCC) are rejected at step 1 and never enter a buffer. These are deterministic and are escalated, not retried — a failed check digit is a data-integrity event, and persistent structural failures feed the Suspect Product Investigation Workflows.
- Hierarchy defects (missing child, extra child, over-capacity, mixed lots) are caught by
validate_tree. The affected container — and only that container — is diverted to a quarantine queue with its full partial map attached, so an operator can reconcile the physical pallet without discarding a shift’s worth of good aggregations. - Transmission failures (L4 repository timeout, broker back-pressure) occur after a valid event exists. These are transient and are retried with exponential backoff and jitter, then routed to a dead-letter queue if they exhaust their attempts. Because every event carries the deterministic idempotency key from step 4, a retry that actually succeeded upstream is de-duplicated rather than double-recorded.
The guiding principle is quarantine-and-continue: isolate the defective unit of work, keep an immutable record of why it was isolated, and let the line run. How aggressively the gate trips a hard line-stop versus a soft alert is a tuned parameter, not a constant, and that tuning is the subject of threshold tuning for line speeds.
Performance & Scalability Considerations
Modern case packers and palletizers push aggregation decisions into a sub-100 ms budget, which shapes every implementation choice:
- Keep the hot path in memory. Open containers live in a bounded in-memory or Redis-backed structure keyed by parent SSCC. The synchronous gate touches only local state; no network call sits between a completed scan and the emit/quarantine decision.
- Partition by parent SSCC. When the transmission stream is fed into a broker (Kafka or RabbitMQ), partition on the parent SSCC so that all events for one container are processed in order on a single consumer. This prevents an
ADDand a later compensatingDELETEfrom racing across partitions while preserving horizontal scale across containers. - Batch the async writes, not the sync validation. Validation is per-container and immediate; repository writes are batched (for example, all cases for one pallet in a single request) to amortize network and persistence overhead without ever delaying the line-side decision.
- Bound the buffer and shed predictably. The edge buffer has a maximum depth; when a downstream outage fills it, the system raises a defined alert and applies back-pressure rather than growing unbounded. Buffer depth is a first-class line-health metric alongside scan success rate and aggregation latency.
Audit & Compliance Checkpoints
Aggregation is a legally binding data contract, so audit readiness is a design property of this stage, not a later reporting step. Every completed aggregation must produce, at minimum:
- An immutable, append-only
AggregationEventrecord with a UTCeventTime, the actingreadPointGLN, and the fullchildEPCslist — hash-chained to the prior entry so any tampering is detectable. - A quarantine audit entry for every diverted container, capturing the validation errors, the partial map, and the operator who resolved it. Manual overrides of a quarantine require dual authentication and generate a distinct audit event flagged for post-shift reconciliation.
- Retention for the DSCSA-mandated six years, reproducible in the exact EPCIS structure originally exchanged, and queryable by identifier. The practical test: name any single SGTIN and the system reconstructs which case and pallet it was aggregated into, when, and where — without manual reconciliation. These controls satisfy 21 CFR Part 11 for electronic records and signatures.
Troubleshooting
| Symptom | Likely cause | Remediation |
|---|---|---|
Downstream verification returns INVALID for units in a shipped case |
AggregationEvent never transmitted, or childEPCs incomplete at emit time |
Replay the edge buffer; confirm the gate required buf.complete before emit; check the DLQ for a stranded event |
| Duplicate parent-child records in the L4 repository | Retry after a partial success without an idempotency key | Enforce the deterministic idempotencyKey; de-duplicate on (parentID, childEPCs) at ingestion |
| Phantom or orphaned child serial | Scanner double-read or dropped read at line speed | Reject duplicates in AggregationBuffer.add; run the shift-change reconciliation job to sweep orphans |
| “Mixed lots in one container” quarantine spikes | Lot changeover not synchronized with pallet build order | Reset the buffer at each lot boundary; align PLC lot signal with the tree builder |
| SSCC collision across parallel lines | Two lines drawing from overlapping serial ranges | Partition the SSCC pool per line/prefix; validate uniqueness at pool checkout, not at emit |
| Line stops on every minor scan miss | Threshold set too aggressively | Tune soft-alert vs. hard-stop counts — see threshold tuning for line speeds |
Frequently Asked Questions
What is the difference between a case SSCC and a pallet SSCC in an aggregation?
Both use GS1 Application Identifier (00) and share the same 18-digit SSCC structure and check-digit rule; the difference is purely their role in the hierarchy. In a pallet-build AggregationEvent the pallet SSCC is the parentID and the case SSCCs are the childEPCs; in a case-build event the case SSCC is the parent and unit SGTINs are the children. Both must be drawn from a serial pool that guarantees global uniqueness so a case and pallet can never collide.
How is an aggregation reversed without breaking DSCSA immutability?
You never edit or delete the original event. To break a hierarchy you append a compensating AggregationEvent with action: DELETE naming the same parent and the children being removed, which disaggregates them while preserving the full, tamper-evident history. Rebuilding a valid hierarchy afterward is handled by the reaggregation path in Decommission & Reaggregation Rules.
Why must every child appear exactly once in childEPCs?
Downstream partners inflate a single scanned SSCC into its full child list by inference. A missing child means product that cannot be accounted for; a duplicated child corrupts counts and can trigger verification failures. Enforcing uniqueness and a completeness check against the manifest before emit is what makes the inference legally trustworthy.
Does aggregation need to happen synchronously with the packaging line?
The tree building and validation decision must be synchronous and local so the line never blocks on the network, but the resulting event is transmitted asynchronously. Keeping the emit/quarantine decision in memory within a sub-100 ms budget, then buffering and publishing events downstream, is what lets a repository or partner outage degrade gracefully instead of stopping the palletizer.
How do I stop network retries from creating duplicate aggregations?
Attach a deterministic idempotency key derived from the parent SSCC and the sorted child list to every event, and de-duplicate on that key at ingestion. Because the key excludes the timestamp, a retried transmission reproduces the same key and is recognized as already-applied rather than written twice.
Related
- Aggregation Hierarchy & Validation Workflows — the parent domain covering nesting, validation gates, and EPCIS event generation.
- Parent-Child Serial Mapping — bidirectional integrity of each link and drift detection.
- Decommission & Reaggregation Rules — safely breaking and rebuilding hierarchies after a fracture.
- Threshold Tuning for Line Speeds — soft-alert vs. hard-stop tuning for high-speed packaging.
- Automating case-to-pallet aggregation validation in Python — the async, broker-backed implementation of this workflow.