Event-Sourced Identity for Seasonal Tire Sets: A Design Exercise in Durable Provenance
Scope, truth frame, and why this is a design exercise
Seasonal tire operations create a deceptively difficult identity problem. A driver may think of “the winter set” as one object, while the physical reality includes four tires, four wheels, valve components, changing axle positions, several storage intervals, inspections, repairs, and eventual retirement decisions. Once a tire is separated from a wheel or moved from left rear to right front, a row named winter_set_2026 no longer tells the whole story.
This article presents a software design exercise, not a description of KMJ Tire’s live production infrastructure. The operational constraints are real, but the event store, schemas, queries, workflows, identifiers, test results, and every numerical example below are illustrative. Nothing here claims that KMJ operates this tool internally, has experienced a related incident, completed a migration, measured adoption, or achieved a performance result.
The exercise is useful because it forces developers to distinguish a durable physical identity from a temporary relationship. Calgary adds realistic pressure: seasonal changes, chinooks, road salt, gravel, highway travel, and large day-night temperature swings create many moments when observations must remain attached to the correct item. For driver-facing background, KMJ’s seasonal tire change overview explains the practical service context, while the system described here remains hypothetical.
The goal is not blockchain, elaborate tracking hardware, or an all-knowing digital twin. It is a modest event-sourced model that can answer ordinary questions without rewriting history:
- Which physical tire did this observation concern?
- Which wheel carried it at that time?
- Where was the assembly mounted on the vehicle?
- What changed during a seasonal service?
- Was a repeated request safely ignored?
- Can current state be rebuilt from accepted facts?
- Can a discrepancy be recorded without silently altering the past?
The core modeling mistake: treating a set as an indivisible thing
A “set” is convenient language, but it is rarely the right aggregate root for every operation. A set can lose one tire to damage, gain a replacement, move between loose storage and vehicle installation, or be split while one component receives further inspection. If membership lives only in four mutable columns, the application tends to erase those transitions.
Consider an illustrative set with tires T1 through T4 and wheels W1 through W4. At the start of autumn, each tire is mounted to a wheel. Later, T3 receives a repair that is appropriate under tire-service criteria. In spring, all four assemblies are removed and stored. Before the following winter, T2 is retired and T5 replaces it. Is this still the same set? For a driver, probably yes. For provenance, the answer must be more precise: the named collection continues, but its membership changes at an effective point in history.
That distinction gives us three identity layers:
- Component identity belongs to an individual tire or wheel.
- Assembly identity represents a tire-wheel pairing during a bounded interval.
- Collection identity groups components for a practical purpose without pretending membership is permanent.
The model should never use a collection identifier as a substitute for component identifiers. It can provide navigation and workflow scope, but it cannot carry every inspection or repair fact. Anyone learning the physical markings that might support intake should review how to read tire sidewall information. The application still needs its own stable identifiers because sidewall attributes are descriptive, not guaranteed database keys.
Identity layers and aggregate boundaries
An event-sourced design benefits from small aggregates with explicit invariants. One possible decomposition is:
| Aggregate | Durable identity | Main responsibility |
|---|---|---|
| Tire | tire_id |
Lifecycle, observations, repair references, retirement |
| Wheel | wheel_id |
Wheel identity and mount relationships |
| Assembly | assembly_id |
Time-bounded tire-to-wheel pairing |
| Vehicle fitment | fitment_id |
Assembly-to-vehicle position interval |
| Storage unit | storage_id |
Custody and location interval |
| Seasonal collection | collection_id |
Named membership over time |
| Service session | session_id |
Correlation, operator workflow, evidence |
The aggregate boundaries do not have to match database tables. They define where consistency must be immediate. For example, a tire aggregate can reject a second active wheel mount. A projection can later combine tire, wheel, and vehicle events for a convenient “current seasonal set” screen.
There is a tradeoff. If every relationship is its own aggregate, cross-aggregate validation becomes asynchronous. If everything sits in one giant vehicle aggregate, contention and history size grow. The design exercise chooses component-focused streams and uses a process manager to coordinate mounting, fitment, and storage transitions.
A service session is not the owner of physical identity. It is a correlation envelope. The same tire exists before and after a service session, and its history should remain continuous. A session may record who observed a sidewall, which measurement method was used, and why an event was proposed. It should not create a new tire merely because a new season began.
Stable identifiers, labels, and evidence
A stable identifier should be opaque. tir_01J... is safer than encoding a vehicle, customer, season, or position into the key. Meaningful identifiers become false when reality changes. A label such as WINTER-LR can be printed or displayed, but it is an alias with its own validity interval.
One illustrative identity record might look like this:
{
"tire_id": "tir_01J8Y7F1A4KX",
"identity_version": 1,
"manufacturer_serial_observed": "example-only",
"dot_code_observed": "illustrative",
"sidewall_size_observed": "example-value",
"created_from_session": "ses_01J8Y7C9P2QH"
}
Those observed fields must not be confused with canonical truth. A technician can mistype a code, a label can become unreadable, and two similar tires can be swapped during intake. Preserve evidence and confidence rather than overwriting a field until it looks clean.
Useful evidence types include a manually entered marking, a barcode assigned by the application, a photograph reference, a prior assembly relationship, and a human-confirmed reconciliation. Images introduce privacy and retention concerns, so the event should store a controlled asset reference and checksum rather than an unrestricted public URL.
The system can express competing observations:
{
"event_type": "TireMarkingObserved",
"tire_id": "tir_01J8Y7F1A4KX",
"field": "sidewall_size",
"value": "illustrative-value",
"confidence": "human_read",
"evidence_ref": "asset_01J8Y81M",
"observed_at": "2026-09-02T16:12:00Z"
}
Load and size are safety-relevant attributes, but software must avoid making unsupported decisions from incomplete data. The tire load index explainer provides practical context. In the design, rules may flag inconsistent records for review; they should not declare a tire suitable solely because one parsed field appears plausible.
An append-only event envelope
Every event needs a predictable envelope separate from its domain payload. The envelope supports ordering, deduplication, tracing, authorization, and schema evolution.
{
"event_id": "evt_01J8Y9D42TVC",
"stream_id": "tire-tir_01J8Y7F1A4KX",
"stream_version": 18,
"event_type": "TireInspectionRecorded",
"schema_version": 2,
"occurred_at": "2026-09-02T16:20:00Z",
"recorded_at": "2026-09-02T16:21:14Z",
"actor": {"type": "staff", "id": "actor_example"},
"correlation_id": "ses_01J8Y7C9P2QH",
"causation_id": "cmd_01J8Y9BZZ6W5",
"idempotency_key": "example-session:tire:inspection:v1",
"payload": {},
"metadata": {
"source": "illustrative-intake-ui",
"design_exercise": true
}
}
occurred_at describes when the physical observation or transition happened. recorded_at says when the event entered the store. Both matter if offline notes are entered later. Stream version provides optimistic concurrency. Event ID provides global uniqueness. Correlation connects related work without merging component streams.
The payload should contain facts, not a cached view of the entire tire. TireInspectionRecorded can carry measurements and observations, but it should not repeat current vehicle, wheel, storage location, and collection membership. Those belong to other events and projections.
Append-only does not mean errors remain unaddressed. A correction is another event that names the superseded observation and explains the reason. The original remains visible to authorized audit tooling.
A vocabulary of physical lifecycle events
Event names should describe facts in past tense. Commands express intent; events state accepted outcomes. A practical vocabulary might include:
TireRegisteredWheelRegisteredTireMountedToWheelTireDismountedFromWheelAssemblyInstalledAtPositionAssemblyRemovedFromVehicleAssemblyMovedToPositionCollectionMembershipStartedCollectionMembershipEndedStorageCustodyStartedStorageCustodyEndedTireInspectionRecordedRepairAssessmentRecordedTireRepairRecordedBalancingServiceRecordedIdentityDiscrepancyRaisedIdentityDiscrepancyResolvedTireRetired
Avoid events like TireUpdated. That label does not reveal what changed and makes replay logic depend on arbitrary snapshots. Also avoid implying that an inspection is a mechanical diagnosis. KMJ performs tire services and oil changes; if an observation suggests a non-tire mechanical concern, the safe workflow is to document the observation and refer the driver to an appropriate mechanical provider.
A repair event should link to an assessment rather than assert that every puncture can be repaired. The practical limitations described in tire repair guidance for Calgary are a reminder that location, condition, and professional assessment matter. The software records the decision and evidence; it does not invent the decision.
Mounting and dismounting without identity loss
Mounting creates a relationship between a tire and wheel. It does not mutate either object into a new component. An assembly identifier can represent the bounded pairing:
{
"event_type": "TireMountedToWheel",
"assembly_id": "asm_01J8YA44X7G2",
"tire_id": "tir_01J8Y7F1A4KX",
"wheel_id": "whl_01J8Y7GQ8F0M",
"mounted_at": "2026-09-02T16:35:00Z",
"session_id": "ses_01J8Y7C9P2QH"
}
The corresponding dismount event closes that relationship. Do not delete the assembly row or reuse its identifier for a later remount. The next pairing receives a new assembly ID, even if the same tire returns to the same wheel. This produces unambiguous intervals and avoids special logic for “temporarily inactive” assemblies.
Core invariants include:
- One tire cannot belong to two active assemblies.
- One wheel cannot belong to two active assemblies.
- A dismount references an active assembly.
- A retired tire cannot enter a new assembly.
- The mount timestamp cannot precede tire or wheel registration.
- An assembly cannot be installed and stored at the same effective time.
These checks happen against aggregate state at command acceptance. A projection may briefly lag, so it is not the right place for authoritative validation.
Balancing belongs to the assembly context because the service concerns the mounted combination at that point. See the practical description of wheel balancing service. A future remount should not automatically inherit a prior balancing record as proof of current state; it remains a historical service fact.
Axles, corners, rotations, and effective intervals
Vehicle position is another temporary relationship. Use a position vocabulary that is explicit for the vehicle configuration, such as front_left, front_right, rear_left, and rear_right for an ordinary four-wheel case. Do not embed position in a tire label as durable identity.
A move can be modeled as one atomic domain event when it occurs within the same vehicle:
{
"event_type": "AssemblyMovedToPosition",
"assembly_id": "asm_01J8YA44X7G2",
"vehicle_id": "veh_example_17",
"from": "rear_left",
"to": "front_left",
"effective_at": "2026-09-02T17:05:00Z",
"reason": "illustrative_rotation_plan"
}
If two assemblies exchange positions, a transaction or process manager must avoid a momentary uniqueness conflict. One approach accepts a RepositionAssemblies command containing the full before-and-after map, validates the permutation, then emits correlated move events. Another creates a single vehicle-fitment event whose payload is the new layout. The first approach offers component-friendly history; the second gives stronger aggregate consistency. Either is defensible if replay semantics remain clear.
An interval projection makes audit questions easy:
| assembly_id | vehicle_id | position | valid_from | valid_to |
|---|---|---|---|---|
| asm_A | veh_17 | rear_left | t1 | t2 |
| asm_A | veh_17 | front_left | t2 | null |
Never infer the previous position only from current data. The event payload should carry enough expected state to detect stale commands. If a user attempts to move asm_A from rear_right while the aggregate says rear_left, reject the command and refresh the workflow.
Storage is custody, not a blank period
Seasonal storage often disappears from simplistic models. The application marks the set “off vehicle” and loses who held it, whether it was loose or assembled, and how identity was verified at retrieval. Treat storage as a custody interval.
StorageCustodyStarted might include:
- the stored item type and identifier;
- a storage unit identifier, not a public location description;
- seal or label evidence where used;
- condition observations at intake;
- the responsible workflow session;
- effective and recorded timestamps.
Retrieval closes the interval with StorageCustodyEnded. A missing or unreadable label should raise an identity discrepancy rather than prompt the operator to select the nearest-looking record.
Calgary’s seasonal context matters even though the data model is generic. Chinooks can produce rapid shifts between dry roads, meltwater, and refreeze. Gravel and road salt leave visible residue. Large day-night swings can affect measured pressure. The system should store the conditions surrounding an observation when relevant, not turn a single measurement into a permanent property.
Collection membership can continue while assemblies are stored, but active fitment cannot. An invariant might read:
FOR EACH assembly:
count(active_vehicle_fitments) + count(active_storage_intervals) <= 1
That rule assumes custody states are exclusive in this design. If a future workflow allows a vehicle stored as a whole, refine the state model instead of adding exceptions in application code.
Inspections as immutable observations
An inspection is a bundle of observations taken at a point in time using a stated method. It is not the tire’s identity, and it should not overwrite prior measurements.
{
"event_type": "TireInspectionRecorded",
"inspection_id": "ins_01J8YB7S0N6A",
"tire_id": "tir_01J8Y7F1A4KX",
"observed_at": "2026-09-02T17:22:00Z",
"measurements": [
{"kind": "tread_depth", "location": "inner", "value": 7, "unit": "illustrative_unit"},
{"kind": "tread_depth", "location": "centre", "value": 8, "unit": "illustrative_unit"},
{"kind": "tread_depth", "location": "outer", "value": 7, "unit": "illustrative_unit"}
],
"observations": ["example_condition_note"],
"design_exercise": true
}
The figures above are illustrative, not advice or a representation of a real tire. Units should be explicit in actual schemas. Measurement devices may also need identifiers and calibration metadata, but only if the organization can maintain those records accurately.
Separate raw observations from derived decisions. A projection can calculate change over time, while a human reviews whether measurement conditions are comparable. Pressure recorded after a cold overnight period is not directly equivalent to a reading after highway travel. Likewise, a tread value without location loses information.
For drivers seeking broad maintenance context, Be Tire Smart guidance is more appropriate than treating sample code as service advice. The hypothetical system exists to preserve facts and workflow provenance, not replace qualified tire inspection.
Repairs, assessments, and retirement
A repair workflow has at least three concepts: reported concern, assessment, and completed tire service. Conflating them creates false certainty. RepairAssessmentRecorded can say that a tire was examined and include the decision category. TireRepairRecorded should only follow an assessment that permits the recorded work.
An example command guard:
function recordTireRepair(state: TireState, cmd: RecordTireRepair) {
if (state.retiredAt) throw new DomainError("TIRE_RETIRED");
const assessment = state.assessments.get(cmd.assessmentId);
if (!assessment) throw new DomainError("ASSESSMENT_REQUIRED");
if (assessment.outcome !== "repairable") {
throw new DomainError("ASSESSMENT_DOES_NOT_PERMIT_REPAIR");
}
if (state.repairs.has(cmd.repairId)) return [];
return [tireRepairRecorded(cmd)];
}
This is illustrative logic. Real validation belongs with current tire-service standards, documented procedures, and professional judgment.
Retirement is terminal for future mounting, but not deletion. TireRetired includes an effective time, reason category, evidence, and actor. Existing projections should remove it from active availability while keeping it in historical queries. If retirement was recorded against the wrong identity, do not “unretire” by editing the event. Raise a discrepancy, append a compensating event under a controlled policy, and retain the audit chain.
Collection membership should end when a retired component leaves the set. The collection can accept a replacement later, making continuity explicit instead of pretending the original four items remain forever.
Collection membership with temporal semantics
A seasonal collection is a useful organizational object. It might be named “winter set for vehicle example,” but the database should avoid making the display name a unique identity rule. Membership is an interval relation:
CREATE TABLE collection_membership_projection (
collection_id text NOT NULL,
member_type text NOT NULL,
member_id text NOT NULL,
valid_from timestamptz NOT NULL,
valid_to timestamptz,
started_by_event_id text NOT NULL,
ended_by_event_id text,
PRIMARY KEY (collection_id, member_type, member_id, valid_from)
);
This allows a query to reconstruct the set as of a past date. It also supports a clean answer when one tire is replaced:
SELECT member_id
FROM collection_membership_projection
WHERE collection_id = $1
AND valid_from <= $2
AND (valid_to IS NULL OR valid_to > $2);
Collection rules should state whether membership applies to tires, assemblies, or both. Mixing them produces duplicates and confusion. A tire-based collection survives remounting to a new wheel. An assembly-based collection is useful when the mounted pair is what must remain together. This design chooses tire membership as the primary seasonal grouping and derives current assemblies.
Driver-facing categories such as all-weather tire information and winter tire guidance can influence how users describe a collection. They should not be hard-coded as provenance assumptions. Classification remains an observed attribute with sources and effective dates.
Idempotency at three different layers
Retries are normal. A tablet may lose connectivity after submitting a command, a queue may deliver twice, or a worker may restart between persistence and acknowledgment. Idempotency must exist at command, event, and projection layers.
Command idempotency uses a key scoped to the intended operation. Store the key with a hash of the command body and the resulting event IDs. A retry with the same body returns the prior result. The same key with a different body is rejected.
Event idempotency enforces unique event_id and (stream_id, stream_version) constraints. Consumers also track processed event IDs.
Projection idempotency applies each event once per projection. A checkpoint alone can be insufficient if writes and checkpoint advancement are not atomic. Place both in one transaction or maintain an inbox table.
BEGIN;
INSERT INTO projection_inbox(projection_name, event_id)
VALUES ('current_assembly', $event_id)
ON CONFLICT DO NOTHING;
-- Continue only when the insert affected one row.
-- Apply deterministic projection changes here.
COMMIT;
Do not derive idempotency keys from timestamps alone. Two distinct observations can share a timestamp, while one operation can be retried with a later timestamp. A workflow-generated UUID or deterministic composition of session, item, and operation is safer.
A domain command may be naturally idempotent. Ending a membership that is already closed can return no new events if the requested closing fact matches. If the effective times differ, the conflict needs review rather than silent acceptance.
Optimistic concurrency and coordinated transitions
An append uses the expected stream version:
INSERT INTO events(stream_id, stream_version, event_id, event_type, payload)
VALUES ($stream, $expected_version + 1, $event_id, $type, $payload);
A unique constraint on (stream_id, stream_version) turns concurrent changes into a detectable conflict. The application reloads state and decides whether to retry. Automatic retry is safe only when the command’s meaning survives the new state.
Mounting a tire to a wheel touches at least two identities. Options include:
- Put both under an assembly coordinator aggregate.
- Use a database transaction that appends to both streams.
- Use a saga that reserves components and compensates on failure.
For a single event store backed by one relational database, a transaction across the relevant streams can be straightforward. For distributed services, reservations and process state may be necessary. Do not introduce a saga merely because it sounds architecturally sophisticated; it multiplies failure states.
The coordinator validates expected versions for both tire and wheel, appends the relationship event, and updates a uniqueness index. An event-store constraint or dedicated active-relationship table prevents two concurrent coordinators from mounting the same component twice.
Reconciliation when physical reality disagrees
Perfect intake is unrealistic. Labels can be damaged, records can be incomplete, and a physical arrangement can differ from a stale projection. Reconciliation must expose uncertainty instead of forcing an operator to manufacture certainty.
Suppose the projection expects tire T3 on wheel W3, but intake evidence suggests tire T3 is on W4. The workflow should:
- Pause transitions involving the uncertain identities.
- Capture observations and evidence separately.
- Raise
IdentityDiscrepancyRaisedwith candidate records. - Require an authorized resolution.
- Append events that explain the accepted mapping.
- Rebuild affected projections and verify invariants.
The discrepancy record can use states such as open, evidence_collected, resolved, and unable_to_resolve. “Unable to resolve” is a legitimate outcome. The software may assign new identity under a controlled policy while retaining a link to candidate histories.
Never rewrite old event payloads to make the stream match the newly discovered arrangement. Some earlier events may have been correct at the time; others may have targeted the wrong record. A correction event should state exactly which assertion is being superseded.
Reconciliation is also where human factors matter. A screen should show distinguishing evidence, not four nearly identical dropdown labels. Color, typography, and layout must not be the only signals. Scanning should confirm the expected item and display a mismatch prominently.
Projection design for daily operations
Event streams are excellent for provenance but awkward for routine reads. Build disposable projections tailored to questions.
Suggested projections:
current_tire_stateactive_assembliesvehicle_position_mapcurrent_storage_custodyseasonal_collection_membersinspection_timelineopen_identity_discrepanciesretired_component_register
Each row stores last_event_id or a checkpoint for traceability. Projection code must be deterministic: the same ordered events produce the same view. Avoid inserting the current processing time into business fields during replay.
A current-state query might join three views:
SELECT
c.collection_id,
c.tire_id,
a.assembly_id,
a.wheel_id,
p.vehicle_id,
p.position,
s.storage_id
FROM current_collection_tires c
LEFT JOIN active_assemblies a ON a.tire_id = c.tire_id
LEFT JOIN current_vehicle_positions p ON p.assembly_id = a.assembly_id
LEFT JOIN current_storage_items s ON s.item_id = a.assembly_id
WHERE c.collection_id = $1;
The result should make impossible states visible. If an assembly appears both installed and stored, flag the projection and stop related workflow steps. Do not let a user interface choose one source arbitrarily.
Calgary service coverage or logistics can be represented independently from component provenance. Public context is available on KMJ’s service areas page and mobile tire service information. In this exercise, those pages are educational links, not evidence that the hypothetical identity system schedules or dispatches anything.
Audit queries that answer real questions
Good audit queries begin with an operational question, not a fascination with raw event counts.
Where has a tire been mounted?
SELECT
payload->>'wheel_id' AS wheel_id,
occurred_at AS mounted_at,
lead(occurred_at) OVER (ORDER BY stream_version) AS next_tire_event
FROM events
WHERE stream_id = $1
AND event_type IN ('TireMountedToWheel', 'TireDismountedFromWheel')
ORDER BY stream_version;
In production, a dedicated interval projection would be clearer than interpreting lead across mixed event types. The example illustrates why event vocabulary and ordering matter.
Which inspections preceded retirement?
SELECT event_type, occurred_at, payload
FROM events
WHERE stream_id = $1
AND occurred_at <= (
SELECT occurred_at FROM events
WHERE stream_id = $1 AND event_type = 'TireRetired'
ORDER BY stream_version DESC LIMIT 1
)
ORDER BY stream_version DESC
LIMIT 20;
Did any component have overlapping active relationships?
SELECT tire_id, a.valid_from, a.valid_to, b.valid_from, b.valid_to
FROM assembly_intervals a
JOIN assembly_intervals b
ON a.tire_id = b.tire_id
AND a.assembly_id < b.assembly_id
AND tstzrange(a.valid_from, a.valid_to, '[)') &&
tstzrange(b.valid_from, b.valid_to, '[)');
Other useful questions include which discrepancies remain unresolved, which projection row came from a given event, and what the collection contained at a historical date. Audit access should be permissioned and logged because records may connect to customer or vehicle information.
Schema evolution without falsifying history
Events outlive application releases. Schema evolution therefore needs explicit policy.
Three common techniques are:
- Upcasting: transform older payloads in memory when read.
-
New event version: introduce
TireInspectionRecordedV2or useschema_version: 2. - New compensating fact: append an event when the business meaning changed.
Use upcasting for representational changes, such as renaming a field or adding a default that was logically true. Do not use it to invent a measurement that was never captured. If version one stored a single tread value and version two stores inner, centre, and outer positions, an upcaster cannot fabricate three values.
Keep upcasters pure and covered by fixtures. A snapshot stores aggregate state at a stream version to speed loading, but it is a cache. The event stream remains authoritative. Snapshot schemas also need versions, and the application must discard an unreadable snapshot and replay.
Event deletion may be required for privacy or legal reasons, which complicates pure append-only claims. A practical design can encrypt sensitive payload portions with per-subject keys, separate personal identifiers from operational component facts, and enforce retention. Seek qualified legal and privacy guidance for real implementation; this article does not prescribe compliance.
Testing invariants with examples and properties
Unit tests should exercise command decisions against reconstructed aggregate state. A table-driven mount test can cover empty, mounted, retired, and stale-version cases.
describe("mount tire", () => {
it("rejects a retired tire", () => {
const state = replay([
tireRegistered("T1"),
tireRetired("T1", "illustrative_reason")
]);
expect(() => decide(state, mount("T1", "W1")))
.toThrow("TIRE_RETIRED");
});
it("emits one event for a valid new pairing", () => {
const state = replay([tireRegistered("T1")]);
const events = decide(state, mount("T1", "W1"));
expect(events).toHaveLength(1);
expect(events[0].event_type).toBe("TireMountedToWheel");
});
});
Property-based tests can generate random valid command sequences and verify:
- no tire has two active assemblies;
- no active assembly has two vehicle positions;
- retirement prevents future mount events;
- replay is deterministic;
- applying any event twice to an idempotent projection changes nothing after the first application;
- collection membership intervals never overlap for the same membership identity;
- every dismount closes an existing active mount.
Model-based tests compare the implementation with a small reference state machine. Mutation tests can confirm that removing an invariant causes at least one test to fail. Fuzz malformed payloads at API boundaries, while trusted domain handlers operate only on validated types.
Use illustrative fixtures clearly named as examples. Avoid copying real customer, vehicle, or service records into test environments. Synthetic records can still represent chinook-driven rescheduling, salt residue observations, or Highway 1 travel context without impersonating real events.
Replay, disaster recovery, and integrity checks
Recovery is more than restoring a database backup. The team needs to know whether ordered streams remain complete and projections can be rebuilt.
A practical rehearsal could follow this sequence:
- Restore an event-store backup into an isolated environment.
- Verify checksums, stream versions, and global ordering assumptions.
- Rebuild every projection from zero.
- Run invariant scans against interval views.
- Compare selected aggregate states with precomputed checkpoints.
- Review open discrepancy records.
- Confirm access controls before exposing the restored environment.
Hash chaining can detect accidental alteration, but it does not make incorrect input true. A simple chain stores event_hash = hash(previous_hash + canonical_event_bytes). Key management, canonical serialization, and backup integrity remain essential. This is not a reason to market the design as tamper-proof.
Replay should be observable. Track the last processed global position, error category, and projection version. The figures used for capacity plans must come from measured workloads in a real environment, not the illustrative examples in this article.
If a projection handler fails on one event, quarantine with care. Skipping it may create a misleading current view. For safety-relevant workflows, block affected records, surface the failure, and repair the handler or data under an auditable procedure.
Observability without inventing business metrics
Instrumentation should describe software behavior and data quality. Useful signals include:
- command rejection counts by invariant code;
- duplicate delivery detections;
- projection lag by global event position;
- open identity discrepancies by age band;
- replay failures by schema version;
- streams with version gaps;
- active-relationship overlap findings;
- events recorded substantially later than they occurred.
Thresholds require local measurement and operational policy. This design exercise does not claim a target latency, throughput, error rate, or existing KMJ baseline. A dashboard showing “99.9% accurate” without a defensible denominator would be worse than no dashboard.
Structured logs should include event ID, stream ID, correlation ID, and error code, while minimizing personal information. Traces can connect the intake command to event append and projection handling. Audit records should distinguish human actions from automated consumers.
Alert fatigue is a design failure. An unresolved identity discrepancy affecting an imminent physical transition deserves a stronger response than a replay job lagging in a non-operational analytics view. Severity should follow impact and recoverability.
Security, privacy, and authority boundaries
Component provenance can become personal data when linked to a driver, address, vehicle identifier, or service history. Apply least privilege and separate concerns.
One boundary might keep tire and wheel streams under opaque vehicle references, with personal details in a different service. Authorized queries join them temporarily. Exports should redact internal actor IDs and evidence asset locations unless needed.
Commands require role-based authorization. Registering an observation, resolving an identity discrepancy, and applying a retirement correction are not equivalent privileges. Sensitive corrections may require a second reviewer.
Event metadata should record the authenticated actor and software client. Do not trust actor fields supplied directly by a browser. The service derives them from verified identity. Evidence assets need signed, expiring access rather than permanent public links.
Threat modeling should include accidental cross-customer association, label substitution, compromised staff credentials, malicious payload replay, and overbroad data export. The physical label is one factor, not unquestionable authority.
Worked history: one tire across two seasons
Consider a synthetic sequence for tire T1. All times and facts are illustrative.
| Version | Event | Resulting interpretation |
|---|---|---|
| 1 | TireRegistered |
T1 exists independently |
| 2 | TireMarkingObserved |
Sidewall evidence attached |
| 3 | CollectionMembershipStarted |
T1 joins collection C1 |
| 4 | TireMountedToWheel |
Assembly A1 pairs T1 with W1 |
| 5 | AssemblyInstalledAtPosition |
A1 sits at rear left |
| 6 | TireInspectionRecorded |
First observation bundle preserved |
| 7 | AssemblyMovedToPosition |
A1 moves to front left |
| 8 | AssemblyRemovedFromVehicle |
Fitment interval closes |
| 9 | StorageCustodyStarted |
A1 enters storage custody |
| 10 | StorageCustodyEnded |
Stored interval closes next season |
| 11 | TireInspectionRecorded |
New measurements remain separate |
| 12 | TireDismountedFromWheel |
A1 relationship ends |
| 13 | TireMountedToWheel |
A2 pairs T1 with W5 |
| 14 | AssemblyInstalledAtPosition |
A2 returns at rear right |
The important result is continuity. T1 remains T1 through position changes, storage, dismounting, and remounting. A1 and A2 are different assemblies because their pairing intervals differ. Collection C1 can still represent the driver’s seasonal group.
Now suppose a later inspection leads to retirement. Versions 15 and 16 could record the inspection and TireRetired. Collection membership ends at version 17. The historical query for the first season still returns T1, while the current collection view does not.
This sequence also supports service education without turning software into a sales claim. Readers comparing tire categories can use KMJ’s all-season tire overview, while developers keep product classification separate from identity history.
Failure modes worth designing before implementation
Several failures recur in provenance systems:
Identity regenerated each season. History fragments because the intake workflow creates new component rows. Fix the lookup and discrepancy process; do not assume absence from the current vehicle means a new item.
Position embedded in identifier. Rotations make identifiers misleading. Use temporal fitment relations.
Set membership stored as four columns. Replacement overwrites history. Use membership intervals.
Inspection row updated in place. Trend and evidence disappear. Append observations and corrections.
Retries create duplicate repairs. Introduce scoped idempotency and unique business identifiers.
Projection treated as authority. Lag or replay bugs permit invalid operations. Validate on aggregate streams and hard uniqueness constraints.
Event payload contains entire mutable state. Conflicting truth spreads across streams. Store the accepted fact and derive views.
Unknown identity forced into nearest match. History becomes confidently wrong. Raise a discrepancy and preserve uncertainty.
Software implies mechanical conclusions. A data flag is presented as diagnosis. Limit the tool to tire-service observations; refer non-tire mechanical concerns to a suitable provider.
Illustrative figures escape into policy. Example thresholds become treated as standards. Mark examples and require approved operational configuration.
A measured implementation path
An organization exploring this pattern should start with the questions it cannot answer reliably, then choose the smallest event vocabulary that resolves them. A possible sequence is:
- Define component identity and labeling policy.
- Model mount, dismount, position, storage, inspection, and retirement facts.
- Write invariants in plain language before code.
- Build aggregate tests and a replayable in-memory prototype.
- Add one current-state projection and one historical query.
- Design discrepancy handling with the people who perform intake.
- Threat-model identity association and evidence access.
- Rehearse duplicate delivery, stale commands, and projection rebuilds.
- Pilot with synthetic records before any real data.
- Establish retention, correction, and incident procedures with qualified guidance.
Do not begin with a grand event catalog or a microservice split. The costly errors are usually semantic: confusing a label with an identity, a current pairing with permanent ownership, or an observation with a decision.
Public pages such as buying tires in Calgary can help teams understand the language drivers encounter. The domain model should still be validated by tire-service professionals and actual workflow users.
Review checklist for the design
Before treating the exercise as implementation-ready, ask:
- Does every tire and wheel keep a stable opaque identity?
- Are labels aliases rather than primary meaning?
- Can mounting, position, storage, and collection intervals be reconstructed?
- Does every correction preserve the original assertion?
- Are commands deduplicated with body verification?
- Can projections consume duplicate events safely?
- Are cross-aggregate transitions protected from concurrency conflicts?
- Can unresolved physical identity remain explicitly unresolved?
- Do inspections preserve method, time, units, and evidence?
- Does retirement prevent future use without deleting history?
- Are schema upcasters forbidden from inventing missing facts?
- Can all projections rebuild deterministically?
- Are personal links separated and permissioned?
- Are non-tire mechanical concerns routed to an appropriate provider?
- Is every sample value labelled illustrative?
- Can an auditor trace a current row back to the source event?
A “yes” does not prove the system is safe or correct. It shows that the team has addressed the main identity and provenance questions deliberately.
Closing perspective: preserve facts, relationships, and uncertainty
Seasonal tire tracking becomes manageable when the model stops pretending that a set is one permanent object. Tires and wheels have durable identities. Assemblies, positions, storage custody, and collection membership are relationships with beginnings and endings. Inspections are observations. Repairs and retirement are explicit lifecycle facts. Discrepancies are first-class work, not embarrassing exceptions to hide.
Event sourcing fits this problem because it preserves transitions and enables historical reconstruction. It also imposes discipline: clear event semantics, idempotent consumers, concurrency control, schema evolution, replay testing, and careful privacy boundaries. A conventional relational history model could also work if it preserves the same facts and intervals. The architectural label matters less than refusing to overwrite identity.
For practical tire-service information rather than software architecture, drivers can review KMJ Tire’s Calgary tire service overview. The system in this article remains exactly what it was at the start: a detailed design exercise grounded in real operational constraints, using synthetic examples and making no claim about KMJ’s internal production tools.











