Article
Modelling Reusable Containers at ecoeats
A takeaway box is a wonderfully simple object
until you want it back
.
Then it needs an identity. And a history. And a way to survive somebody replacing its label without becoming, administratively speaking, a completely different box.
At ecoeats, reusable containers weren't just a checkbox on an order. The code modeled individual physical containers and the observations linking them to returns, washing, pickups, and shipments.
The satisfying bit wasn't putting a QR code on plastic. It was working out what that scan actually meant.
The box number and lifecycle diagram are illustrative. They are not records from the service.
Identity belongs to the object, not the sticker
A label is how the system recognises a container. It isn't necessarily the container itself.
Labels get damaged. Codes can be replaced. If replacing a sticker creates a new asset record, you lose the continuity you were trying to build.
The container model separated the physical object's identity from alternative codes associated with it. That makes a useful general distinction for any physical asset system: identity is durable; the identifier you can currently read may not be.
It applies just as well to rented equipment, library items, or reusable shipping crates. A new label shouldn't erase an old history.
A scan is an observation, not omniscience
Imagine a box is scanned at a return point at 09:00. At 09:01, someone moves it.
Your database still knows where it was observed at 09:00. It does not suddenly know where the box is now.
That sounds obvious until you build a field called
currentLocation
and let everyone forget what it really represents.
The useful distinction is between physical identity, observed events, and derived state. The original model had separate container and scan records, with contextual relationships for different parts of the return process. I'd preserve that separation rather than squeezing everything into one status column.
A small model with honest names
Here's an illustrative design, not the original schema:
type Container = {id: string;typeId: string;};type ContainerCode = {code: string;containerId: string;retiredAt?: string;};type Observation = {id: string;containerId: string;occurredAt: string;recordedAt: string;kind: 'returned' | 'received-for-washing' | 'dispatched';workflowId: string;};
occurredAt
and
recordedAt
answer different questions. A scanner may upload late. A server may receive two observations in an order different from the physical events.
That doesn't require a grand event-sourcing architecture. It requires you to avoid pretending that insertion order is automatically reality.
Duplicate scans aren't all the same mistake
Scanning the same box twice during one return should probably not count as returning two boxes.
Scanning that box again after another complete use cycle is entirely legitimate. A permanent unique constraint on the container ID would stop the business working after the first return. Very efficient, in a terrible way.
Deduplication needs a scope: the return session, shipment, workflow, or explicit idempotency key. The original scan model included uniqueness rules tied to particular contexts rather than treating every observation as interchangeable.
For a new design, decide the operation's meaning first. Then make the database enforce it atomically. A “does this exist?” query followed by an insert still races when two devices do it together.
The missing scan is the interesting case
The happy path is almost suspiciously neat: issued, used, returned, washed, issued again.
Real workflows contain gaps. A return point might have an unrecognised code. A shipment might contain fewer containers than expected. A box might be physically present without the expected earlier observation.
I'd model those as explicit reconciliation work. Keep the observation that actually happened, flag the inconsistency, and give an authorised person a way to resolve it with an explanation.
Don't invent the missing steps just to make the diagram look tidy. You will eventually use that tidy diagram to make a financial or operational decision.
Track less about people, more about the workflow
Asset tracking can quietly become people tracking if every scan accumulates identity and precise location indefinitely.
Store the context needed for the operation, restrict access, and choose retention periods deliberately. A demonstration doesn't need real customer identities or locations, and neither does most debugging.
Likewise, scanning a code isn't proof that the scanner is authorised to claim a reward or change custody. Identifier possession and permission are different concerns.
The return journey is the product
Software can't make someone return a box by maintaining a beautiful relational schema. The physical process still has to be convenient.
But the schema can support a convenient process, or fight it. Durable identities, scoped deduplication, and honest observations let you handle the messy bits without losing track of what you know.
The box comes back. Eventually. The database should be ready for both words.