EV ID tracks physical assets across their whole lifecycle — transfers, repairs, and rebuilds. The core insight: an asset’s identity must be independent of every mutable attribute, including serial numbers that get replaced with the motherboard.
Identity vs. attributes
- The UUID is minted once, at first registration, and never changes.
- Serial numbers, owners, locations, and statuses are attributes attached to the identity — all replaceable.
- Physical labels (QR) encode only the UUID, so relabeling is never required when attributes change.
The asset core stays minimal on purpose
interface Asset { id: string; // UUID v4, immutable createdAt: string; } interface AssetEvent { assetId: string; // FK -> Asset.id seq: number; // monotonic per asset type: 'registered' | 'transferred' | 'serviced' | 'status_changed'; payload: Record<string, unknown>; at: string; } Append-only history as the source of truth
Current state is a projection over the event list, cached in a read model. Disputes ("who had this device in March?") are answered by the log, not by reconstructing from backups. This mirrors event sourcing, scaled down to what a small system actually needs.
Keeping lookups fast
Read model refreshed on write
-- Projection table: one row per asset, updated in the same tx as the event INSERT INTO asset_current (asset_id, owner_id, status, updated_at) VALUES ($1, $2, $3, now()) ON CONFLICT (asset_id) DO UPDATE SET owner_id = EXCLUDED.owner_id, status = EXCLUDED.status, updated_at = EXCLUDED.updated_at; Scanning a QR resolves the UUID to the projection row in one indexed lookup, while the full history remains available one join away. Simple, auditable, and boring in the best way.
Originally published on dogukankardas.com. The full case study is here: EV ID. I build software for small businesses and SaaS teams — see more at dogukankardas.com.