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.

Double-opening leather champagne case, closed

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.