Schema Drift: Intermediate
What you will be able to do
Schema Registries: Where They Live
Recognize what a schema registry stores and how compatibility checks block incompatible producer changes.
What a Registry Stores
| Element | Purpose | Example |
|---|---|---|
| Subject | A logical name for a stream of related schemas | checkout_events_v |
| Schema | The Avro, Protobuf, or JSON Schema document | Record with fields user_id, amount, currency |
| Version | Monotonic integer assigned per subject | 1, 2, 3, with version 4 being the latest |
| Schema ID | Globally unique identifier embedded in each message | Short integer prefixed to the serialized payload |
| Compatibility mode | Rule the registry enforces on new versions | BACKWARD, FORWARD, FULL, or NONE |
How Compatibility Checks Run
- Schema documented in a wiki page that no one reads
- Compatibility checked by hope and code review
- Old payloads in flight after a schema change cannot be parsed
- Consumer breakages discovered in production after the producer ships
- Schema is a versioned artifact with an immutable ID
- Compatibility checked at registration; incompatible schemas rejected
- Each message carries its schema ID; consumers can parse any version
- Breaking changes are blocked at the producer, not at the consumer
Schema IDs Embedded in Messages
- ▸Producers cannot ship incompatible schemas; the registry rejects them
- ▸Consumers can parse any version that ever existed in the topic
- ▸Audit log: every schema change is timestamped and attributable
- ▸Cross-team contracts: producer and consumer teams share one source of truth
A registry is to schemas what a version control system is to code. Without it, schemas drift through tribal knowledge. With it, every shape that has ever flowed through a topic can be reconstructed, audited, and reasoned about.
The Expand-Contract Pattern
Apply the four phases of expand-contract to plan a breaking schema change that does not require a synchronized deploy.
The Four Phases
| Phase | Producer State | Consumer State |
|---|---|---|
| 1. Expand | Add new shape; keep old shape | Continue reading old shape |
| 2. Dual-write | Populate both shapes for every event | Begin reading new shape, validate against old |
| 3. Migrate | Continue dual-writing | Cut over to new shape one consumer at a time |
| 4. Contract | Drop old shape | All consumers on new shape; old shape unused |
A Worked Rename
When Expand-Contract Is Necessary
Why It Beats a Flag Day
- All teams deploy at the same instant
- One slow team blocks everyone else
- Rollback is all-or-nothing
- Only viable at small organizations
- Teams deploy independently across weeks
- Slow teams extend the dual-write phase, not block the change
- Rollback is per phase: revert just the latest deploy
- Standard practice at any organization with more than a few consumer teams
- ▸Phase 2 must validate that both shapes carry equivalent data; drift between them is a bug
- ▸Phase 3 must track which consumers have migrated; without a registry of consumers, the producer cannot know when phase 4 is safe
- ▸Each phase must hold long enough that observed traffic confirms it is stable
- ▸Rollback plan: every phase must be reversible without destroying data
- Treat expand-contract as four deploys, not one
- Backfill historical rows during the expand phase so the new field has a complete history
- Add a dual-write consistency check that fails the build if the two shapes drift
- Skip the dual-write phase under time pressure; without it, rollback is impossible
- Drop the old shape before confirming all consumers have migrated
- Apply expand-contract to a change that did not require it; the cost is not free
Schema evolution stays safe when a compatibility check sits between producer and topic: adding a column is backward-compatible and passes; a rename or type change is breaking and is caught before it reaches consumers.
Event Time Versus Processing Time
Distinguish event time from processing time and pick the correct domain for a given consumer query.
The Two Domains, Restated
| Property | Event Time | Processing Time |
|---|---|---|
| Source | Stamped by the producer or device | Stamped by the engine on receipt |
| Stability | Fixed property of the event forever | Depends on producer, broker, and engine lag |
| Reproducibility | Reruns produce the same buckets | Reruns may produce different buckets if lag changed |
| Correctness for history | Correct: the event happened when it happened | Wrong: late events land in the wrong bucket |
| Implementation cost | Engine must track watermarks and hold state | Engine just reads its own clock |
Why the Two Diverge
Worked Example: Same Events, Two Domains
- ▸Reports keyed on real-world time (revenue per hour, sessions per day)
- ▸Auditable systems where re-running must produce identical history
- ▸Any system that compares producer-side metrics to consumer-side metrics
- ▸Operational metrics about the engine itself (events per second processed)
- ▸Real-time alerting where 'when the engine saw it' is the question
- ▸Coarse cost monitoring where small lateness is in the noise
Apache Beam, Flink, and Spark Structured Streaming all expose event-time semantics as a first-class concept. The Beam programming model in particular treats every aggregation as having an explicit WindowFn and a TimestampFn. This is not academic. It is the only way an engine can answer the question of which window an out-of-order event belongs to without inspecting every event individually.
Watermarks: The Engine's Promise
Recognize a watermark as the engine's promise about no-earlier events and reason about how it closes a windowed aggregation.
What a Watermark Actually Is
| Property | Description |
|---|---|
| Timestamp | An event-time value that advances over time |
| Promise | No events with event_time < watermark will be admitted to a window |
| Effect | Windows whose end is below the watermark are eligible to close and emit |
| Source | Computed from the input stream; falls behind when sources are slow |
| Per-source vs global | Each input has its own watermark; the engine takes the minimum |
How the Watermark Closes a Window
Watermark Strategies
| Strategy | How It Computes | When To Use |
|---|---|---|
| Bounded out-of-orderness | watermark = max_event_time_seen - allowed_lag | Most cases; assumes lateness has a known upper bound |
| Ascending timestamps | watermark = max_event_time_seen | Events arrive in strict order; rare but cheap when true |
| Punctuated | Watermark embedded in special marker events from the producer | Producer can signal end-of-stream segments explicitly |
| Custom | Application-specific function | When the source has unusual lateness characteristics |
- ▸Guarantees: events older than the watermark have already been routed to their windows
- ▸Does NOT guarantee: every event that will ever arrive has been seen
- ▸Does NOT guarantee: the watermark advances monotonically (it can stall on slow sources)
- ▸Implication: events arriving after the watermark are 'late' and require an explicit policy
- Windows close on processing time, dropping late events silently
- Reruns produce different answers as lag conditions change
- Cannot reason about correctness of historical results
- Engine state grows unbounded if windows never close
- Windows close when the engine knows no earlier events will arrive
- Reruns are reproducible; the watermark is a function of the input data
- Late-arriving events are surfaced as a separate signal, not silently dropped
- Engine state bounded: closed windows release their state
Watermarks turn the late-data problem from a quality bug into an explicit policy parameter. The team chooses how long to wait, the engine enforces the wait, and any event arriving after the watermark is handled by a named policy rather than a silent drop.
1-Hour Allowed Lateness
Configure a streaming aggregation with allowed lateness and reason about what happens to on-time, slightly-late, and very-late events.
The Configuration
The Three Cases
| Case | Event Time | Arrival Time | Lateness | What Happens |
|---|---|---|---|---|
| On time | 09:02:14 | 09:02:18 | 4 seconds | Joins window [09:00, 09:05); window closes at watermark = 09:06 |
| Slightly late | 09:02:14 | 09:34:00 | 32 minutes | Window already closed; allowed lateness admits it; aggregation re-fires with updated total |
| Very late | 09:02:14 | 11:15:00 | 2 hours 13 minutes | Past allowed lateness; event is dropped from the streaming output and routed to a dead-letter stream |
Walking the Timeline
The Tradeoff Costs
- ▸Measure the lateness distribution from production: 95th, 99th, and 99.9th percentile
- ▸Set allowed lateness above the 99th percentile to capture the bulk of late events
- ▸Expect state cost roughly proportional to allowed lateness divided by window size
- ▸Plan for the truly-late tail: a separate batch reconciliation job, covered in the advanced tier
- Tune allowed lateness against measured lateness percentiles, not guesses
- Route past-lateness events to a dead-letter stream so they are not silently lost
- Make downstream sinks idempotent on the window key to absorb late re-emissions
- Set allowed lateness to infinity; engine state grows without bound
- Set allowed lateness to zero; the long tail of mobile retries is silently dropped
- Forget that the watermark can stall when an upstream partition goes idle
> A growth-stage analytics platform runs a streaming pipeline that ingests checkout events from Kafka, aggregates revenue per minute, and writes to a Snowflake table powering a real-time pricing dashboard. The product team requests two changes the same week. First, the checkout event payload must change: the legacy 'fare_cents' field is replaced with a structured 'pricing' object containing base, surge, and tax. Second, analysts complain that the streaming dashboard consistently reads 1.6 percent below the next-day batch reconciliation. The new tech lead is asked to design a single rollout that handles both.
Schema registries and watermarks turn ad-hoc tolerance into engineered guarantees
- Category
- Pipeline Architecture
- Difficulty
- intermediate
- Duration
- 32 minutes
- Challenges
- 0 hands-on challenges
Topics covered: Schema Registries: Where They Live, The Expand-Contract Pattern, Event Time Versus Processing Time, Watermarks: The Engine's Promise, 1-Hour Allowed Lateness
Lesson Sections
- Schema Registries: Where They Live (concepts: paSchemaEvolution)
The beginner tier treated schemas as an implicit contract. The intermediate tier turns that contract into a system of record. A schema registry is a service that stores schema definitions, assigns each one an immutable version, and runs compatibility checks before accepting a new version. Producers register the schema before publishing data under it. Consumers fetch the schema by version when they read. The registry is the single source of truth for what shape data should have at each moment in
- The Expand-Contract Pattern (concepts: paSchemaEvolution)
The additive default works for most schema changes. It does not work for the breaking ones. When a column must be renamed, dropped, or restructured, every consumer downstream has to adapt. Doing this in a single deploy is impossible at any reasonable scale. The expand-contract pattern is the production technique for rolling out a breaking change without a flag day. It splits the change into four phases that allow producers and consumers to migrate independently. The Four Phases Each phase is a d
- Event Time Versus Processing Time (concepts: paStreamProcessing)
The beginner tier introduced event time and processing time as a pair. The intermediate tier turns the distinction into the foundation of every streaming aggregation. The choice of which time domain to use is not a stylistic preference. It is the single most consequential decision in a streaming pipeline. It determines what numbers the consumer sees, how the system handles late events, and how much state the engine has to keep around. The Two Domains, Restated Most production streaming systems h
- Watermarks: The Engine's Promise (concepts: paLateData)
If a streaming engine waits forever for late events, no window ever closes and no result is ever produced. If it does not wait at all, every late event is dropped and every aggregation is wrong. The watermark is the compromise. A watermark is a timestamp that the engine emits, periodically, declaring that no events with event_time earlier than the watermark will be processed against an open window. The watermark is the engine's commitment to a closing rule. What a Watermark Actually Is A waterma
- 1-Hour Allowed Lateness (concepts: paLateData)
The pieces from the prior sections combine into a single concrete configuration. A streaming aggregation tumbles in 5-minute event-time windows, with a watermark using bounded out-of-orderness of 60 seconds, plus an allowed lateness of 60 minutes. This configuration shows up in production at scale; the numbers are tuned per workload. The walkthrough below traces what happens to events that arrive on time, slightly late, and very late. The Configuration The configuration says four things. Windows