Designing an Event-Sourced Tire Maintenance State Machine for Calgary Fleets
Fleet maintenance software often begins as a spreadsheet with one row per vehicle. A coordinator overwrites “winter tires installed” with “all-season tires installed,” updates a tread number, and moves on. That model is easy to understand, but it destroys the sequence that explains how the current condition arose. It cannot reliably answer whether an inspection happened before a seasonal change, which measurement was corrected, or why an alert disappeared.
An event-sourced design takes the opposite approach. It records every accepted fact as an immutable event, then derives the current state by replaying those facts in order. The result is not merely a longer log. It is a disciplined state machine whose transitions can be tested, audited, rebuilt, and explained.
This article develops that design for Calgary fleets. The examples focus on tire service and oil-change coordination because those are the services KMJ Tire performs. Mechanical concerns involving alignment, brakes, suspension, steering, or diagnostics belong with an appropriate mechanical facility. The boundary matters in both operations and software: a state model should never turn an observation into a service promise the business does not make.
The Calgary setting adds useful pressure to the architecture. Chinooks can swing temperatures quickly. Vehicles may run Deerfoot Trail at commuting speeds, crawl through yards, cross Stoney Trail in heavy wind, or head west on Highway 1. Gravel season, road salt, deep cold, and large day-night temperature changes all affect what operators observe. Software needs to preserve those observations without pretending that a database can diagnose a tire.
Start with decisions, not database fashion
Event sourcing is worthwhile only when history changes decisions. A small fleet that needs a simple reminder may be well served by ordinary tables. A larger operation benefits when several people enter measurements, corrections arrive late, vehicles move between depots, or proof of the sequence matters.
Write the questions first. Can an operator explain why a unit is marked “inspection required”? Can the system distinguish a low-pressure observation made during a cold morning from a later verified reading? Can a corrected odometer value repair derived intervals without erasing the original submission? Can an auditor see which rule version produced a seasonal readiness flag?
Those questions establish three products of the system:
- an immutable history of accepted observations and actions;
- a deterministic projection representing current operational state;
- an explanation trail connecting each derived status to evidence and rules.
Do not describe an event stream as a digital technician. It stores evidence and supports workflow. A person still examines the tire and decides what physical service is appropriate. Drivers looking for plain-language fundamentals can use KMJ Tire’s Be Tire Smart guide, while the fleet system should retain the structured facts behind internal decisions.
The architectural test is simple: if rebuilding from the same events under the same rule set produces different state, the implementation is not deterministic enough. Hidden wall-clock reads, mutable lookup values, and undocumented manual overrides are usual causes.
Define the aggregate around a physical fitment
A vehicle is an obvious aggregate, but it is often too broad. Tires change positions, wheel assemblies are swapped, and a trailer may be disconnected from a tractor. Conversely, making every tire its own aggregate complicates rules that depend on axle or vehicle context.
A practical compromise is a VehicleTireFitment aggregate. It represents the set of tire positions active on one vehicle over a bounded period. It begins when a fitment is installed or imported and ends when that set is removed, superseded, or retired. Each physical casing can have an optional asset identifier, yet operational state is computed in vehicle context.
The aggregate identity might be:
tenant_id + vehicle_id + fitment_id
Position identifiers should be explicit rather than inferred from array order. Examples include LF, RF, LR_OUTER, and RR_INNER. A configuration record describes the valid positions for that vehicle. If a fleet records load information, the model should preserve the observed or declared value and link users to a clear explanation of tire load index; it should not silently calculate a safe configuration from incomplete inputs.
Keeping the fitment boundary narrow improves concurrency. A pressure reading for Unit 17 does not contend with an oil-change entry for Unit 83. It also gives replay a natural stopping point. Historical fitments remain queryable without forcing every current-state query to traverse the vehicle’s lifetime.
The aggregate owns transition validity, not every fleet policy. “A removal cannot precede installation” belongs inside it. “Send a winter-readiness review list in September” belongs in a projection or policy process.
Build an event vocabulary from observable facts
Good event names use past tense and represent something that happened. Avoid commands disguised as events, such as InspectTires. Prefer InspectionRecorded, which states that a record was accepted. Avoid vague containers like VehicleUpdated; they guarantee painful archaeology later.
A useful initial vocabulary includes:
FitmentInstalledPressureObservedTreadMeasuredSidewallConditionObservedRotationRecordedBalanceServiceRecordedRepairAssessmentRecordedTireServiceCompletedSeasonalChangeRecordedOdometerObservedObservationCorrectedFitmentRemovedFitmentRetired
Some names deserve caution. A RepairAssessmentRecorded event can capture “repairable,” “not repairable,” or “requires physical review” only when that result came from an authorized assessment. A driver reporting a screw should produce SidewallConditionObserved or a general damage observation, not a definitive repair outcome. Guidance about what can influence a repair decision belongs in a resource such as tire repair information for Calgary, not in an overconfident automated rule.
Events should capture source provenance: who or what produced the observation, which device or form version was used, and when it was observed. “Recorded at” is not enough because offline yard inspections may sync hours later. That distinction becomes essential for late data.
Treat event vocabulary as a public API inside the organization. Review proposed additions for semantic overlap. A smaller set of precise facts ages better than dozens of subtly different status flags.
Separate commands, events, and projections
Three layers keep responsibilities legible. Commands express intent, events record accepted outcomes, and projections answer queries. Mixing them produces circular logic.
Consider this command:
{
"type": "RecordTreadMeasurement",
"vehicleId": "unit-017",
"fitmentId": "fit-2026-spring",
"position": "LF",
"depth32nds": 7,
"observedAt": "2026-07-31T14:20:00-06:00",
"idempotencyKey": "mobile-81f0"
}
The command handler loads the aggregate, validates identity and position, checks authorization, and emits TreadMeasured. The event store appends it only if the expected stream version still matches. A projection then updates the current tread view and any evidence-based review queue.
The projection is disposable. If its schema changes, rebuild it from the stream. The event is not disposable. If an event schema is wrong, use upcasting or a compensating event rather than rewriting history.
This separation also keeps links and education outside the critical write path. A dashboard can direct an operator to sidewall marking information when a size field looks unfamiliar. The event handler only checks that submitted fields conform to the approved schema; it does not scrape web content or embed changing prose into historical facts.
Commands may fail. Events describe success. Projections can lag. Those three truths should appear in metrics and user messages so an accepted observation is not confused with an immediately refreshed dashboard.
Model transitions as explicit legal moves
A state machine should be more precise than a status column. At minimum, model lifecycle, evidence completeness, and operational flags separately. A single enum cannot express that a fitment is active, has a recent inspection, and awaits human review.
One lifecycle can be:
UNINITIALIZED -> ACTIVE -> REMOVED -> RETIRED
FitmentInstalled moves an uninitialized stream to active. Measurement and observation events are legal only while active, except a correction that targets an earlier event. FitmentRemoved closes active use. FitmentRetired can follow removal when a casing or assembly is no longer tracked for future service.
Operational flags are derived rather than transitioned directly:
needs_review = unresolved_damage_observation
OR stale_required_measurements
OR rule_validation_failure
The handler must reject impossible moves with machine-readable reasons. For example:
FITMENT_NOT_ACTIVE
POSITION_NOT_CONFIGURED
OBSERVATION_TIME_OUT_OF_RANGE
TARGET_EVENT_NOT_FOUND
CONCURRENCY_CONFLICT
Avoid an all-purpose “force” option. If exceptional authority is necessary, model an explicit override event containing the actor, reason, scope, expiry if relevant, and evidence reference. That preserves accountability.
State diagrams are useful, but executable transition tests are better. Every event type needs cases for valid state, invalid state, boundary timestamps, duplicate delivery, and replay from an empty stream.
Use an envelope that can survive ten years
Payload fields evolve, while a stable envelope supports routing and audit. The envelope should carry identity, ordering, causation, correlation, and schema information without relying on payload conventions.
{
"eventId": "01J45YQXQW8P5Y0TQ2FZP6K6N2",
"eventType": "TreadMeasured",
"schemaVersion": 2,
"tenantId": "fleet-a",
"aggregateType": "VehicleTireFitment",
"aggregateId": "unit-017:fit-2026-spring",
"streamVersion": 38,
"occurredAt": "2026-07-31T14:20:00-06:00",
"recordedAt": "2026-07-31T20:03:11Z",
"actor": {"type": "employee", "id": "emp-42"},
"source": {"system": "yard-mobile", "version": "4.8.1"},
"correlationId": "inspection-run-992",
"causationId": "cmd-01J45YQWJ3",
"idempotencyKey": "mobile-81f0",
"payload": {"position": "LF", "depth32nds": 7}
}
Use an absolute timestamp with offset for occurredAt and normalize a second copy to UTC where query infrastructure requires it. Keep the supplied offset because it helps humans reason about a Calgary workday. Store the IANA zone, America/Edmonton, on tenant or depot configuration; offsets alone do not encode daylight-saving rules.
Never put mutable employee names into the identity field. The actor identifier remains stable, while a separate directory projection renders the current or historical display name. Similar reasoning applies to vehicle labels.
Personal data should be minimized. Tire state rarely needs a driver’s home address or private notes. Event immutability makes unnecessary data especially costly to govern.
Design the event store for append-only truth
PostgreSQL can support this workload without exotic infrastructure. The central table needs a unique event identity and a unique stream version.
create table tire_event (
tenant_id text not null,
aggregate_id text not null,
stream_version bigint not null,
event_id uuid not null,
event_type text not null,
schema_version integer not null,
occurred_at timestamptz not null,
recorded_at timestamptz not null default now(),
actor jsonb not null,
source jsonb not null,
correlation_id text,
causation_id text,
idempotency_key text not null,
payload jsonb not null,
primary key (tenant_id, event_id),
unique (tenant_id, aggregate_id, stream_version),
unique (tenant_id, idempotency_key)
);
create index tire_event_stream_scan
on tire_event (tenant_id, aggregate_id, stream_version);
create index tire_event_occurred_scan
on tire_event (tenant_id, occurred_at, event_id);
Append inside one transaction. Read the current maximum stream version, compare it with the command’s expected version, insert the event, and add an outbox record. If another writer won the race, return a concurrency conflict and let the application reload before deciding whether a retry is meaningful.
Database permissions should deny update and delete to the application role. Retention and privacy procedures may require carefully governed exceptions, but routine software must not mutate history. Backups should be restored in drills, not merely declared healthy.
Partitioning by tenant or recorded month may help at scale. Choose it after measuring stream sizes and replay patterns. Premature partitions create operational burden without improving correctness.
Make idempotency a domain feature
Mobile networks around industrial yards and highway corridors are imperfect. A user may tap submit twice, a queue may redeliver, or a worker may reopen an offline form. Exactly-once delivery is not a realistic end-to-end assumption. Idempotent handling is the practical answer.
Require a stable idempotency key generated when the user begins the submission, not when a retry occurs. Scope it to the tenant. When the same key returns with an identical normalized command, return the earlier success. When it arrives with different content, reject it as an idempotency conflict and surface both hashes for investigation.
def handle(command):
normalized = canonical_json(command.business_fields)
digest = sha256(normalized)
prior = idempotency_repo.find(command.tenant_id, command.key)
if prior:
if prior.command_hash != digest:
raise IdempotencyConflict(prior.event_id)
return Accepted(prior.event_id, duplicate=True)
stream = store.load(command.aggregate_id)
events = decide(stream.state, command)
return store.append(events, expected_version=stream.version,
command_hash=digest)
Canonicalization rules need versioning. Decide whether 7, 7.0, and "7" are equivalent before hashing. Trim permitted whitespace, normalize enum case, and reject ambiguous units. Do not quietly convert a measurement whose unit is missing.
Consumer idempotency is separate. Each projector stores the last event or stream position it applied. An inbox table keyed by consumer and event ID prevents duplicate side effects. This distinction matters because an idempotent write does not guarantee idempotent downstream processing.
Validate measurements without inventing certainty
Validation has layers. Schema validation checks types and required fields. Domain validation checks configured positions and lifecycle. Plausibility validation flags values that deserve review. It should not automatically transform an unusual reading into a normal one.
For tread depth, store the reported unit and normalized value. If the form supports thirty-seconds of an inch and millimetres, preserve both the original submission and conversion metadata. Rounding policy belongs in a named rule version. The same principle applies to pressure units.
Pressure readings are sensitive to temperature and timing. A cold-soaked morning value and a reading after a Deerfoot run are not interchangeable. Capture context fields such as:
- measurement method and device identifier;
- whether the vehicle had recently operated;
- ambient temperature if actually measured or sourced;
- observation location or depot, at an appropriate precision;
- placard reference entered or selected by the operator;
- notes restricted to operational facts.
The system may flag an implausible range, but it should preserve the submitted observation in a quarantine workflow. Deleting it conceals a training problem or failing sensor. Automatically declaring a tire safe is outside the role of data validation.
When a dashboard explains balancing concepts, link to the wheel balancing service overview. A vibration report can trigger review; it cannot establish its own cause. Mechanical diagnosis remains outside this state machine.
Treat late-arriving data as normal, not exceptional
An inspection completed at 07:10 may sync at 13:00 after the tablet reconnects. Event-store order reflects when records were accepted, while business chronology reflects when observations occurred. Both are valuable, and neither should overwrite the other.
Always assign stream version in recorded order. Reordering historical events after append breaks stable offsets, replication, and audit references. Instead, projections that care about observed chronology maintain an ordered timeline using (occurred_at, event_id) and recompute the affected window when an older observation arrives.
Suppose these records are appended:
v40 12:05 recorded PressureObserved occurred 11:58
v41 13:02 recorded TreadMeasured occurred 07:10
v42 13:04 recorded ObservationCorrected targets v41
The current-state projector sees versions 40, 41, and 42 in that sequence. Its tread reducer applies the correction to the measurement’s logical record. A chronology view displays the corrected 07:10 observation before the 11:58 pressure entry. The raw ledger still shows the actual acceptance sequence.
Set a configurable late-data horizon for expensive temporal projections. An observation older than that horizon can enter a review queue rather than triggering an unbounded rebuild. The event remains stored either way.
Offline clients should display pending, accepted, or rejected status clearly. A locally saved form is not yet part of the authoritative stream.
Correct facts with lineage instead of erasure
Humans transpose digits. Devices are assigned to the wrong unit. A useful immutable system makes correction straightforward without pretending the initial record never existed.
ObservationCorrected should identify the target event, the fields being superseded, the corrected values, a reason code, and the responsible actor. Restrict correction to fields that are legally correctable. Changing a timestamp may be allowed with evidence; changing the original actor should generally require a distinct administrative process.
{
"targetEventId": "01J45YQXQW8P5Y0TQ2FZP6K6N2",
"changes": {"depth32nds": {"from": 7, "to": 8}},
"reasonCode": "TRANSCRIPTION_ERROR",
"evidenceRef": "inspection-sheet-2026-07-31-unit17"
}
Reducers should expose effective values alongside lineage:
effective_depth = 8
source_event = ...K6N2
correction_event = ...P91A
original_depth = 7
Do not use corrections to reverse a valid service action. A mistakenly attributed seasonal change may require a voiding or attribution-correction event with stricter authorization. Domain language prevents casual edits from distorting history.
Reports should count corrections and conflicts as quality signals. A rising rate may reveal confusing forms, weak unit labels, or unreliable device assignment. The answer is often workflow improvement, not hiding the evidence.
Project current state without losing explanation
The principal read model can use one row per active fitment position plus an aggregate summary. Store enough references to explain every displayed field.
create table current_tire_position (
tenant_id text not null,
vehicle_id text not null,
fitment_id text not null,
position_code text not null,
effective_tread numeric,
tread_unit text,
tread_event_id uuid,
effective_pressure numeric,
pressure_unit text,
pressure_event_id uuid,
last_observed_at timestamptz,
needs_review boolean not null default false,
projection_version bigint not null,
primary key (tenant_id, vehicle_id, fitment_id, position_code)
);
An explanation endpoint can return the rule, evidence events, and human-readable reason behind needs_review. For example, “recent damage observation has no resolving assessment” is more useful than a red badge. Never phrase a system flag as a physical diagnosis.
Other projections can serve different jobs: seasonal readiness, inspection completeness, service history, correction audit, and data-quality monitoring. Their schemas should reflect the query rather than mirroring the event payloads.
Users deciding among seasonal categories may need neutral education on all-season tires in Calgary, all-weather tire considerations, and winter tire use. Those resources inform people; the projection only reports recorded fitment facts and configured policy results.
Track projector lag in seconds and events. An apparently stale read model should be visible to operators before they act on it.
Represent Calgary temperature context honestly
Temperature is valuable context, but it is easy to misuse. Weather-station data may be kilometres from a vehicle, and a vehicle inside a heated bay does not share outdoor conditions. Treat environmental data as attributed observations, not ground truth.
A TemperatureContextAttached event or enrichment record can include source, station identifier, sampling time, distance estimate, and value. If the temperature came from a handheld device at the tire, distinguish it from public weather data. Never backfill an unlabelled number as if someone measured it onsite.
Chinooks create rapid transitions. A fleet dashboard could group pressure observations by cold-soaked, recently operated, indoor, or unknown context. The grouping helps comparison without applying a universal correction formula. Tire pressure guidance depends on the vehicle manufacturer’s information and actual conditions.
Road salt and freeze-thaw cycles can be represented as operational context only when a fleet has a defined inspection process. Similarly, a Highway 1 west route tag indicates exposure and duty pattern; it is not evidence of tire condition. Gravel travel may justify a configured observation cadence, yet an event should record the inspection outcome rather than infer damage.
Rule engines must store the environmental input IDs they used. If a vendor corrects historical weather data, existing decisions stay reproducible under their original evidence while analysts may run a clearly labelled revised projection.
This is the difference between an auditable model and a clever-looking dashboard whose answers shift invisibly.
Encode seasonal policy as versioned rules
Seasonal readiness is a fleet policy question, not a single event. A policy evaluates active fitment facts, service history, vehicle class, routes, and dates. Its output should identify the version and evidence.
policy_id: calgary-seasonal-readiness
version: 2026.2
effective_from: 2026-08-01
inputs:
- active_fitment_category
- last_tread_observation
- unresolved_condition_observations
- vehicle_route_class
outputs:
- READY_FOR_REVIEW
- EVIDENCE_INCOMPLETE
- REVIEW_REQUIRED
The intentionally cautious labels matter. Software can determine that required evidence exists. It cannot guarantee how a tire will perform in every storm or road condition. A person makes the service decision.
Store policy evaluation as a derived record containing input event IDs. Whether to persist it as a domain event depends on consequences. If it triggers a legally meaningful workflow, persistence may be justified. If it merely powers a dashboard filter, a rebuildable projection is cleaner.
Seasonal change history should be explicit. A SeasonalChangeRecorded event identifies removed and installed fitments, observed date, service reference, and positions. Fleet coordinators can compare that record with practical seasonal tire change information, while the application avoids making an invented timing promise.
Test rules against Calgary edge cases: an early September cold snap, a warm chinook interval, a unit temporarily assigned west of the city, and a measurement captured just before midnight local time.
Publish events safely with an outbox
The database append and message publication must not form an unreliable dual write. Insert the domain event and an outbox row in one transaction. A relay publishes outbox entries to the broker, then marks delivery progress. Consumers remain idempotent because brokers may redeliver.
create table event_outbox (
outbox_id bigserial primary key,
tenant_id text not null,
event_id uuid not null,
topic text not null,
payload jsonb not null,
created_at timestamptz not null default now(),
published_at timestamptz,
attempts integer not null default 0,
unique (tenant_id, event_id, topic)
);
Monitor oldest unpublished age rather than queue length alone. Ten stuck messages can be more serious than ten thousand moving normally. Record retry causes and place poison messages into a quarantined path after a defined threshold; do not drop them.
Avoid placing mutable projection results on the domain-event topic. Consumers should either receive facts or explicitly versioned integration messages. That boundary prevents a downstream analytics system from treating a temporary flag as permanent truth.
For webhooks, sign payloads, include event IDs, and document retry behaviour. Receiving systems should use those IDs as inbox keys. Rotate signing secrets through a managed process and keep them out of event payloads, repositories, and proof reports.
Audit access, rules, and human overrides
Immutability alone does not create a trustworthy audit. You also need to know who could append which events, which rules ran, and who viewed sensitive operational data.
Use role-based command authorization. A driver may record an observation, a trained service user may record an assessment, and an administrator may correct attribution. Authorization decisions should be logged with policy version and outcome, without copying secrets or excessive personal information.
Every manual override needs structured fields:
- override type and affected aggregate;
- reason from a controlled vocabulary plus concise notes;
- actor and authorization policy;
- supporting evidence reference;
- creation and optional expiry time;
- link to the decision or flag being superseded.
Audit queries must work without direct production database access. Build a read-only audit view that joins event lineage, corrections, and rule evaluations. Exported evidence should include hashes or stable IDs so later comparisons can detect accidental alteration.
Review privileged access periodically. Test that an ordinary application role cannot update event rows. Simulate a failed append, broker outage, duplicate command, and projector rebuild. A paper policy that has never met a failure is not operational assurance.
Test reducers with properties and hostile sequences
Example-based tests cover expected stories. Property-based tests reveal unexpected sequences. Generate commands and events under constrained rules, then assert invariants after replay.
Useful properties include:
- replaying a stream twice yields identical state;
- duplicate delivery to a projector does not change its result;
- stream version rises by exactly one per accepted event;
- no measurement becomes current before its fitment is active;
- corrections retain access to original and effective values;
- a removed fitment cannot accept a new ordinary observation;
- projection rebuild matches the online projection at the same checkpoint.
Hostile cases deserve deliberate fixtures. Send a correction before its target reaches a partitioned consumer. Deliver versions 19, 21, then 20. Repeat an event after a process crash between state update and checkpoint commit. Submit a local daylight-saving timestamp without an offset. Reuse an idempotency key with a different vehicle.
Keep golden streams as plain fixtures, but do not let them become the only coverage. Schema upcasters need contract tests using every historical version still present in production. Measure replay time with realistic payload sizes and tenant distribution.
An end-to-end test can begin with installation, add observations, apply a correction, remove the fitment, rebuild all projections, and verify every explanation points to an existing event.
Operate replay as a controlled production change
Rebuilding a projection sounds harmless because the output is disposable. In production, it consumes database capacity, can expose old schema assumptions, and may briefly produce incomplete answers. Treat replay like a deployment.
Build into a new projection version or shadow table. Record the source checkpoint, code version, rule versions, start time, finish time, event count, and checksum summary. Compare old and new results using expected-difference rules before switching readers.
Throttle by database health, not an arbitrary sleep. Pause if replica lag or query latency crosses a threshold. Tenant-level checkpoints let the operator resume without restarting the entire fleet population.
Snapshots may speed aggregate loading, but they are caches. A snapshot records aggregate ID, stream version, state schema version, and checksum. If loading fails validation, discard it and replay from the stream. Never make snapshots the only surviving representation.
Observability should cover append latency, concurrency conflicts, duplicate rates, correction frequency, event age at ingestion, outbox delay, consumer lag, dead-letter volume, rebuild duration, and explanation lookup errors. Segment metrics by source system while protecting tenant privacy.
Runbooks should say what operators may safely pause, rebuild, or retry. They should also identify when a tire observation needs human service review rather than software intervention.
Keep service boundaries visible in the product
Fleet platforms often expand until every vehicle concern looks like a work order for the same provider. Resist that drift. KMJ Tire’s service scope is tire work and oil changes. If a driver reports pulling, braking behaviour, steering changes, or a suspension concern, the application should route the concern to an appropriate mechanical facility instead of implying KMJ will diagnose or repair it.
This boundary can be encoded in a capability registry:
{
"provider": "KMJ Tire",
"capabilities": ["TIRE_SERVICE", "OIL_CHANGE"],
"unsupportedMechanicalRouting": "REFER_OUT"
}
Capabilities should be configuration backed by approved business facts, not inferred from event names. A VibrationObserved event may exist because fleets need the history. Its workflow can recommend physical review without assigning a cause or unsupported service.
When users need local context, a directory-oriented page such as Calgary’s local tire service overview can be presented outside the audit trail. For organizations comparing broader fleet support, commercial tire services and fleet management information provide human-readable background.
Product language is part of correctness. “Evidence requires review” is defensible. “Problem fixed” is not defensible unless a recorded service action and verification support it.
A practical implementation sequence
Begin with one fitment stream and three observation events. Establish envelope, append transaction, optimistic concurrency, idempotency, and a rebuildable current-state projection before adding brokers or elaborate rules.
The next increment should introduce corrections and explanation lineage. That forces the team to confront immutability honestly. Then add an outbox, one downstream consumer, and measured replay. Only after these foundations behave under failure should seasonal policy or environmental enrichment enter the system.
A sensible delivery sequence is:
- write event definitions and invariants in plain language;
- implement aggregate decisions and reducer tests;
- create append-only storage with database restrictions;
- expose idempotent command endpoints;
- build current state plus an explanation endpoint;
- add correction lineage and audit views;
- introduce outbox delivery and consumer inboxes;
- version seasonal rules and Calgary context inputs;
- automate shadow replay and comparison;
- rehearse recovery using production-shaped test data.
Keep the user interface honest during each stage. Show when data was observed, when it was accepted, whether a projection is current, and which evidence supports a flag. An operator looking for general tire options can separately browse tires available for Calgary drivers; the event system should not invent inventory claims.
The durable advantage is not fashionable architecture. It is the ability to answer, with evidence, what was known, what changed, which rule applied, and why the current state looks the way it does. In Calgary fleet operations, where weather, routes, people, and timing rarely line up neatly, that clarity is worth designing deliberately.
Walk through one fitment from installation to archive
A concrete stream exposes design gaps that isolated schemas can hide. Imagine Unit 17 receives a tracked fitment at a Calgary depot on October 6. The command carries a vehicle configuration version, the four position identifiers, recorded tire category, and service reference. Validation confirms that the fitment ID is unused and all required positions are represented. FitmentInstalled becomes version one.
At 06:40 the next morning, a yard worker records pressure observations while the vehicle has been parked overnight. Four commands share an inspection correlation ID but have distinct idempotency keys. The application accepts three immediately. The fourth is retried after a network interruption; its key and normalized content match the earlier acceptance, so the API returns the original event ID rather than appending another fact.
Two weeks later, a driver reports visible damage at the right rear position after travel through an active gravel area. The event stores the driver’s observation and image reference. It does not decide repairability. A projection sets needs_review and explains that an unresolved condition report exists. A trained physical assessment later records its outcome. The resolving event references the original report, allowing the projection to clear or retain the flag according to an explicit rule.
In January, an offline inspection submits a tread measurement several hours late. Its occurredAt time places it before another same-day observation, although its stream version is higher. The chronology projection recalculates that day’s ordering, while the append ledger remains unchanged. An analyst can see both the operational sequence and the ingestion delay.
During data review, the coordinator finds that the January value was entered against LR rather than RR. Authorization permits a position correction because the signed inspection sheet supports it. ObservationCorrected points to the source event and records only the changed position field. Current-state rows for both positions are rebuilt from the affected point, and the correction-quality metric increments.
When the fitment is removed in spring, FitmentRemoved closes ordinary observations. A delayed mobile submission observed before removal may still be accepted under a documented late-data policy; a new observation made after removal is rejected. This distinction uses observation time, submission provenance, and policy version rather than a simplistic check against the current clock.
Finally, the assemblies are retired from fleet tracking. A retention process later moves cold stream segments to lower-cost storage while preserving hashes, indexes, and restore instructions. The audit view still resolves event lineage. Nothing in this lifecycle required overwriting a status field, guessing why a value changed, or treating a workflow flag as a mechanical conclusion.
That narrative should become an executable acceptance test. Seed the commands with fixed IDs and timestamps, capture the resulting stream, rebuild projections from zero, and compare explanations with expected evidence. Run the same fixture after every reducer, schema, and policy change. If a migration alters a conclusion, the test should require an explicit expected-difference record rather than a casually updated snapshot.
Top comments (0)