Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Backwards Compatibility

Maintaining backwards compatibility lets you evolve protocols without breaking existing deployments. It’s often the difference between smooth upgrades and painful migrations.

Compatibility Types

Backwards Compatible:
  New servers work with old clients.
  Client v1 ──────> Server v2 ✓

Forwards Compatible:
  Old servers handle new clients gracefully.
  Client v2 ──────> Server v1 ✓ (degraded)

Full Compatibility:
  Both directions work.
  Ideal but not always achievable.

Techniques for Compatibility

Ignore Unknown Fields

// Client v1 sends:
{ "name": "Alice", "age": 30 }

// Server v2 expects:
{ "name": "Alice", "age": 30, "email": "?" }

// Server should:
// - Accept missing email (use default or null)
// - Not reject the request

// Client v2 sends:
{ "name": "Bob", "age": 25, "email": "bob@example.com" }

// Server v1 should:
// - Ignore unknown "email" field
// - Process name and age normally

Optional Fields with Defaults

// Protocol Buffers example
message User {
  string name = 1;
  int32 age = 2;
  optional string email = 3;  // Added in v2
}

// Missing optional fields get default values.
// Old messages work with new code.
// New messages work with old code (email ignored).

Extensible Enums

Bad: Fixed enum, no room to grow
  enum Status { OK = 0, ERROR = 1 }

Good: Reserve unknown handling
  enum Status {
    UNKNOWN = 0,  // Default for unrecognized
    OK = 1,
    ERROR = 2
    // Future: PENDING = 3
  }

Old code receiving new status → UNKNOWN (handled gracefully)

Reserved Fields

message User {
  string name = 1;
  reserved 2;  // Was 'age', removed in v3
  string email = 3;
  reserved "age";  // Prevent reuse of name
}

// Prevents accidentally reusing field numbers
// which would cause data corruption.

Breaking Changes

Sometimes breaking changes are necessary:

What's Breaking:
  - Removing required fields
  - Changing field types
  - Renaming fields (in JSON)
  - Changing semantics of existing fields
  - Removing supported message types

Mitigation Strategies:
  1. New endpoint/message type (keep old working)
  2. Deprecation period with warnings
  3. Version bump (v1 → v2)
  4. Feature flags during transition

Postel’s Law (Robustness Principle)

"Be conservative in what you send,
 be liberal in what you accept."

Send: Strictly conform to spec
Accept: Handle variations gracefully

This enables interoperability between
implementations with slight differences.

Testing Compatibility

# Test old client against new server
$ old-client --server=new-server --test-suite

# Test new client against old server
$ new-client --server=old-server --test-suite

# Fuzz testing with version mixing
$ compatibility-fuzzer --versions=v1,v2,v3

# Contract testing
$ pact-verify --provider=server --consumer=client-v1

Real-World Examples

JSON (excellent compatibility):
  - Unknown fields ignored
  - Missing fields → null/default
  - Easy to extend

Protocol Buffers (good compatibility):
  - Field numbers provide stability
  - Unknown fields preserved
  - Wire format stable

HTTP (exceptional compatibility):
  - 20+ years of evolution
  - HTTP/1.1 still works everywhere
  - Headers ignore unknown values