Skip to main content

Introduction

Applications inevitably change over time:
  • New features are added
  • Requirements change
  • Bug fixes are deployed
  • Performance improvements are made
In most cases, a change to an application’s features also requires a change to the data it stores. The challenge: How do we manage schema changes when old and new code need to coexist? Key concepts:
  • Backward compatibility: New code can read data written by old code
  • Forward compatibility: Old code can read data written by new code
This chapter explores how different encoding formats handle schema evolution.

1. Formats for encoding data

When you want to send data over the network or write it to a file, you need to encode it as a sequence of bytes.

Language-specific formats

Many programming languages have built-in encoding (Python’s pickle, Java’s Serializable, Ruby’s Marshal). Problems:
  • Tied to one language
  • Security issues (arbitrary code execution)
  • Poor versioning support
  • Inefficient
Avoid language-specific formats for data that needs to outlive a single process.

JSON, XML, and CSV

Text-based formats that are human-readable and language-independent. Advantages:
  • Human readable
  • Language independent
  • Widely supported
Disadvantages:
  • Ambiguity with numbers
  • No binary string support
  • Verbose, larger size
  • Schema support varies
Problems with JSON/XML:

Binary encoding

Text formats are verbose. Binary formats are more compact and faster to parse. Size comparison:

2. Thrift and Protocol Buffers

Binary encoding formats that require a schema. Benefits:
  • Compact binary encoding
  • Type safety
  • Schema evolution support
  • Code generation

Thrift

Schema definition (Thrift IDL):
Key insight: Fields are identified by tag numbers (1, 2, 3), not field names. This enables schema evolution.

Protocol Buffers

Similar to Thrift, developed by Google. Schema definition (Protocol Buffers):
Varint encoding - compact representation of integers: Small numbers use fewer bytes!

3. Schema evolution

The key to maintaining compatibility during schema changes.

Adding fields

Rule: New fields must be optional or have default values for backward compatibility.

Removing fields

Rules for removing fields:
  1. Can only remove optional fields (never required!)
  2. Cannot reuse tag number (forward compatibility!)

Changing field types

Some type changes are safe (like int32 to int64 for forward compatibility), but they may lose precision in backward compatibility. Always test thoroughly.
Example of safe type change:

4. Avro

A different approach to schema evolution, developed for Hadoop. Avro difference: No tag numbers in schema! Avro schema (JSON):

Writer’s schema vs reader’s schema

Avro’s key insight: Need both schemas to decode data! How Avro resolves schemas: The reader compares the writer’s schema with its own schema and maps fields by name. Schema resolution rules:
  • Writer field present, reader field present: Match by name, convert if types compatible
  • Writer field present, reader field absent: Ignore field
  • Writer field absent, reader field present: Use default value or null

Schema evolution in Avro

Adding a field:

Where does writer’s schema come from?

Different contexts:
  • Large file with many records: Include writer schema once at file start
  • Database with schema registry: Store schema version in each record, schema registry maps ID to schema
  • Network service: Negotiate schema at connection setup
Schema registry example:

Avro vs Thrift/Protocol Buffers

Thrift/Protocol Buffers:
  • Tag numbers enable field renaming
  • Schema embedded in code
  • Can’t reuse tag numbers
  • Manual tag management
Avro:
  • No tag numbers to manage
  • Can rename fields with aliases
  • Better for dynamic schemas
  • Need writer’s schema to decode
Field aliases in Avro:

5. Modes of dataflow

How does data flow between processes? Three main modes:
  1. Via databases: Process writes, another reads later
  2. Via services: Client sends request, server responds
  3. Via message passing: Async message queue

Dataflow through databases

Old code might inadvertently delete new fields when rewriting records. Always preserve unknown fields!

Dataflow through services (REST and RPC)

Two main approaches: REST and RPC REST example:
RPC example (gRPC):
RPC hides the differences between network calls and local calls, which can make debugging harder. Network calls are unpredictable, may fail, may timeout, and idempotency matters.
API versioning:

Dataflow through message passing

Benefits of message queues:
  • Decoupling: Sender doesn’t need to know about receiver
  • Buffering: Queue absorbs bursts
  • Reliability: Retry on failure
  • Flexibility: Multiple consumers
Example systems: RabbitMQ, Apache Kafka, Amazon SQS Message format:
Schema evolution in messages:

Actor model

Each actor:
  • Has a mailbox for incoming messages
  • Processes messages sequentially
  • Can send messages to other actors
  • No shared state (concurrency-safe)
Examples: Akka (JVM), Orleans (.NET), Erlang

Summary

Key takeaways:
  1. Encoding formats trade-offs:
    • JSON/XML: Human-readable, widely supported, verbose
    • Thrift/Protobuf: Compact, fast, requires schema
    • Avro: Most compact, dynamic schemas, complex resolution
  2. Schema evolution essentials:
    • New fields must be optional or have defaults
    • Can’t remove required fields
    • Can’t reuse field tags/IDs
    • Preserve unknown fields when possible
  3. Compatibility requirements:
    • Backward: New code reads old data (common)
    • Forward: Old code reads new data (harder)
    • Both needed for rolling deployments
  4. Dataflow patterns:
    • Databases: Long-lived data, many readers
    • Services: Request-response, synchronous
    • Messages: Asynchronous, decoupled
  5. Best practices:
    • Include schema version in data
    • Test compatibility between versions
    • Use schema registry for centralized management
    • Document breaking changes