Menu

Topic 8 of 8

Handling Schema Evolution

Learn Handling Schema Evolution for free with explanations, exercises, and a quick test (for API Engineer).

Published: January 21, 2026 | Updated: January 21, 2026

Why this matters

As an API Engineer, your systems will change: new fields get added, types evolve, names get refined. If you change schemas without a plan, you break clients, pipelines, and dashboards. Handling schema evolution lets you ship changes safely and keep integrations stable.

  • Real tasks you will face: add optional fields to events without breaking consumers; rename API properties with zero downtime; split a database column while keeping reads and writes working; deprecate enums across services.
  • Success looks like: zero-breaking deploys, consumers unaffected, easy rollbacks, and a clear deprecation path.

Who this is for

  • API Engineers and Backend Developers maintaining public/internal APIs and event schemas.
  • Data/Platform engineers owning contracts between services and data pipelines.

Prerequisites

  • Basic understanding of REST/JSON or gRPC/protobuf.
  • Familiarity with databases and simple migrations.
  • Comfort with CI/CD and gradual rollouts.

Concept explained simply

Schema evolution is how you change the shape of your data (fields, types, names) over time without breaking running systems.

  • Backward compatible: new producers work with old consumers (old readers can read new data).
  • Forward compatible: old producers work with new consumers (new readers can read old data).
  • Fully compatible: both directions work.

Safe changes are usually additive (adding optional fields with defaults). Risky changes remove or change meaning. Plan changes in two phases:

  • Expand: make the system accept both old and new shapes.
  • Contract: remove old shape after every consumer has moved.

Mental model

Think of change as a bridge with two lanes:

  • Write-path: what producers send/store.
  • Read-path: how consumers interpret it.

Always upgrade readers before writers. Readers should tolerate unknown fields and missing optional fields. When confident all readers can handle new shape, update writers to emit it. Finally, clean up the old path.

Core patterns and rules

Expand-Contract (a.k.a. Parallel Change)
  1. Expand: allow old and new fields simultaneously; keep defaults.
  2. Migrate: backfill or dual-write so both representations exist.
  3. Contract: remove the old field only after all consumers switch.
Compatibility rules (JSON/Avro/Protobuf)
  • Add fields as optional with defaults.
  • Never reuse removed field identifiers (for binary formats like Protobuf/Avro).
  • Do not change semantics without a new field name.
  • For enums: only add values; never remove or repurpose existing ones.
API versioning strategy
  • Prefer in-place compatible evolution when possible.
  • Use header or path versioning only for breaking changes you cannot stage.
  • Provide deprecation warnings and sunset timelines.
Database online migration pattern
  1. Expand: add new column/table, write to both.
  2. Backfill: copy historical data to new shape.
  3. Cutover: switch reads to new column/table.
  4. Contract: stop writing old column; drop it later.
Event schema evolution
  • Use upcasters in consumers to map old events to new models.
  • Emit new fields as optional; keep old fields during migration.
  • If replaying, prefer idempotent consumers and versioned handlers.

Worked examples

Example 1: Add an optional field to a JSON event

Goal: Add user.locale to UserCreated event.

  1. Expand readers: ensure consumers ignore unknown fields or map user.locale with default "en".
  2. Expand producers: start emitting user.locale when available.
  3. Monitor: error rate and consumer lag.
  4. Contract: none needed if field remains optional.
Before/After payload
{
  "event": "UserCreated",
  "userId": "123"
}

{
  "event": "UserCreated",
  "userId": "123",
  "user": { "locale": "en" }
}

Example 2: Rename an API field without downtime

Goal: Rename fullName to name in a REST response.

  1. Expand: return both fields; keep them consistent.
    { "id": 7, "name": "A Lee", "fullName": "A Lee" }
  2. Notify: deprecate fullName with a clear timeline.
  3. Cutover: after clients migrate, stop including fullName.
  4. Contract: remove fullName from code and tests.

Example 3: Split a column in the database

Goal: Split address into street and city.

  1. Expand: add new columns (street, city); keep address.
  2. Dual-write: when receiving updates, populate both.
  3. Backfill: parse address into street/city for existing rows.
  4. Switch reads: update service to read street/city only.
  5. Contract: stop writing address; drop column later.
Self-check
  • Are writes idempotent if backfill runs twice?
  • Can readers handle partially backfilled rows?

Example 4: Enum evolution in Protobuf

Goal: Add enum value PREFERRED to ContactMethod.

  • Safe: add a new enum value at a new numeric tag; do not reuse numbers.
  • Readers: default unknown values to a safe behavior (e.g., treat as EMAIL).

Step-by-step change playbooks

  1. Inventory consumers: list services, jobs, and dashboards relying on the schema.
  2. Choose compatibility target: backward-only, forward-only, or both.
  3. Design expand state: dual-read/dual-write, defaults, upcasters.
  4. Roll out readers first: deploy tolerant parsers and feature flags.
  5. Roll out writers: start producing the new shape.
  6. Backfill and verify: run checksums, sample comparisons, and canaries.
  7. Cutover and contract: flip reads, then remove legacy paths.
  8. Post-change: add lints/guards to prevent regressions.

Exercises

Do these and compare with the solutions below. The Quick Test at the end helps you check understanding. Everyone can take it; only logged-in users get saved progress.

Exercise 1: Plan an additive change safely

Scenario: A GET /orders response needs a new optional field deliveryWindow { start, end } without breaking existing mobile apps that ignore unknown fields.

  • Classify the change (compatibility type).
  • Write an expand-contract plan in 5–7 steps.
  • List 3 monitoring checks during rollout.
Show solution

Classification: Backward compatible (old readers ignore unknown fields). Forward compatible w.r.t old producers.

  1. Inventory apps and confirm they ignore unknown fields.
  2. Add deliveryWindow to response behind a flag.
  3. Deploy readers (server-side mappers) tolerant to missing fields.
  4. Enable flag for 5% traffic; watch error rates and payload sizes.
  5. Gradually ramp to 100%.
  6. Add contract tests ensuring field remains optional with defaults.
  7. Document deprecation policy (none required here).
  • Monitoring: 4xx/5xx rates by client version; response size p95; mobile crash analytics.

Exercise 2: Rename a field across API, DB, and events

Scenario: customer.fullName should become customer.name. There is a Postgres table, a REST response, and a CustomerUpdated event used by analytics.

  • Provide an end-to-end plan covering read/write paths, dual-write, and backfill.
  • Define a rollback plan.
Show solution
  1. DB expand: add name column; keep full_name.
  2. Service write-path: on updates, write both name and full_name.
  3. Read-path: map DB to API returning both name and fullName.
  4. Event expand: emit both fields in CustomerUpdated; mark fullName deprecated.
  5. Backfill: set name = full_name for all rows; re-emit events if needed for analytics.
  6. Consumer upgrades: dashboards/jobs switch to name.
  7. Cutover: API stops returning fullName; event drops fullName; service reads name only.
  8. Contract: stop writing full_name; drop column after a sunset period.

Rollback: keep dual-write code; if issues, revert to reading full_name and re-enable fullName in API/events. Because expand state remains compatible, rollback is safe.

  • I confirmed consumers handle unknown fields
  • I have a backfill and a rollback plan
  • I monitor errors and schema compliance during rollout

Common mistakes and self-check

  • Changing meaning without a new name: always introduce a new field when semantics change.
  • Removing fields before auditing consumers: inventory first, contract last.
  • Not versioning enums/IDs in binary formats: never reuse tags.
  • Skipping reader-first rollout: update parsers before producers.
  • Backfill without idempotency: make backfills re-runnable and verifiable.
Self-check prompts
  • If you deploy writers first, who breaks and why?
  • Can you roll back without data loss?
  • What proves migration completeness (e.g., counts, checksums)?

Practical projects

  • Build a tiny service that evolves a user profile schema through three changes: add optional field, rename, split column. Include scripts for backfill and contract tests.
  • Create an event upcaster library that maps old events to the latest model; include unit tests for unknown fields.
  • Implement a feature-flagged rollout that dual-writes to two tables and flips reads via a config toggle.

Learning path

  • Before this: API contract testing; CI/CD basics; data modeling.
  • Now: schema compatibility, expand/contract, online migrations.
  • Next: consumer-driven contracts, CDC-based migrations, replay strategies and idempotent processing.

Mini challenge

Your payments service must change amount from integer cents to decimal with currency. Draft a 6–8 step expand-contract plan covering API, DB, and events. Include monitoring and a rollback trigger.

Next steps

  • Take the Quick Test below to check your understanding. Available to everyone; only logged-in users get saved progress.
  • Apply one worked example to a side project and practice a full expand-contract cycle.

Glossary

  • Expand/Contract: phased change adding compatibility first, removing legacy last.
  • Upcaster: code that upgrades older messages to the current model when reading.
  • Dual-write: temporarily writing both old and new representations.
  • Backfill: migrating historical data to the new shape.

Practice Exercises

2 exercises to complete

Instructions

Scenario: A GET /orders response needs a new optional field deliveryWindow { start, end } without breaking existing mobile apps that ignore unknown fields.

  • Classify the change (compatibility type).
  • Write an expand-contract plan in 5–7 steps.
  • List 3 monitoring checks during rollout.
Expected Output
A short plan with compatibility classification, 5–7 rollout steps, and 3 monitoring items.

Handling Schema Evolution — Quick Test

Test your knowledge with 10 questions. Pass with 70% or higher.

10 questions70% to pass

Have questions about Handling Schema Evolution?

AI Assistant

Ask questions about this tool