The first article in this series modeled a product as a data model. The second one dealt with a curved object that broke the rectangular assumption.
This one is worse: a case that holds three objects — one bottle, two champagne flutes — each with a different shape, a different tolerance, and its own cavity. And they all have to share one outer envelope.
Here is what that does to the schema.
The single-item model stops working immediately
The earlier model had one dimensions block and one interiorProfile. For a multi-item set, "the interior" is not one thing — it is a layout.
The naive fix is to add fields: bottleCavity, fluteCavityLeft, fluteCavityRight. That works for exactly one configuration and collapses the moment a client asks for two glasses instead of one, or a wider bottle.
The better fix: model cavities as a list.
type Cavity = {
id: string; // 'bottle', 'flute-left', 'flute-right'
holds: 'bottle' | 'flute' | 'accessory';
inner: Dimensions;
clearanceMm: number; // how much room around the object
position: 'centre' | 'left' | 'right';
orientation: 'upright' | 'lying';
};
type MultiItemSpec = {
envelope: Dimensions;
wallThicknessMm: number;
cavities: Cavity[];
sharedToleranceMm: number;
};
Now the configuration is data. Two flutes, one flute, three miniatures — all the same schema.
The constraint that actually matters
With one item, the only check is "does it fit". With multiple items, the constraint is additive:
sum(cavity widths) + dividers + 2 × wallThickness ≤ envelope width
That sounds obvious, but it is the check that gets skipped, because each cavity is designed independently by a different person. The bottle cavity is specced by the packaging engineer; the flute cradles are specced by the structural designer. Nobody runs the sum until tooling is quoted.
Here it is as a validator:
function validateMultiItem(s: MultiItemSpec): string[] {
const errors: string[] = [];
const totalCavityWidth = s.cavities.reduce(
(sum, c) => sum + c.inner.widthMm + c.clearanceMm * 2,
0
);
// Dividers between adjacent cavities, plus the outer walls.
const dividers = Math.max(0, s.cavities.length - 1);
const required =
totalCavityWidth + dividers * s.wallThicknessMm + s.wallThicknessMm * 2;
if (required > s.envelope.widthMm) {
errors.push(
`interior needs ${required}mm but envelope is ${s.envelope.widthMm}mm`
);
}
// Every cavity must have a distinct position or they overlap.
const positions = s.cavities.map((c) => c.position);
if (new Set(positions).size !== positions.length) {
errors.push('two cavities share the same position');
}
return errors;
}
Run it against a real spec and it tells you the case is 8mm too narrow — before anyone cuts a die.
Why the error message matters more than the check
The valuable part is not the boolean. It is the message:
interior needs 268mm but envelope is 260mm
That single line is what turns a two-week email thread into a five-minute fix. A validator that returns false is useless in a supply chain; a validator that returns the missing millimetres gets acted on.
The general pattern, again
Across all three articles the same shape keeps appearing:
- Keep the shared fields shared: envelope, wall thickness, tolerance, finish, quantity.
- Push the variable part into a list: profiles for curved products, cavities for multi-item sets.
- Validate the additive constraint, and return the number, not the boolean.
A packaging spec is not a form. It is a small constraint problem wearing a form's clothes.
The case shown is a double-opening leather champagne case from TANCYBOX — one bottle, two flutes, three cavities, one envelope.

Top comments (0)