Unit 06.04: Versioning a contract without breaking callers
Compatibility breaks in a direction people consistently get backwards.
Seven changes, four breaking
Additions, removals and enum widenings, sorted by whether they break callers.
The code lists them.
CHANGES = [
("add an optional response field", "safe", "old clients ignore it"),
("add an optional request field", "safe", "old clients omit it"),
("add a REQUIRED request field", "BREAKING", "every existing caller now fails"),
("remove a response field", "BREAKING", "clients reading it break"),
("widen an enum you RETURN", "BREAKING", "clients switching on it break"),
("widen an enum you ACCEPT", "safe", "old values still valid"),
("rename a field", "BREAKING", "remove plus add"),
]
print(f"{'change':38} {'kind':10} why")
for change, kind, why in CHANGES:
print(f"{change:38} {kind:10} {why}")
breaking = sum(1 for _, k, _ in CHANGES if k == "BREAKING")
print(f"\n{breaking} of {len(CHANGES)} require a new version path")
# The two enum rows point in opposite directions, which is the one people get
# wrong. Accepting more is safe; returning something new breaks any client that
# branches on the value.
The two enum rows point in opposite directions. Accepting a new value is safe - old callers simply never send it. *Returning* a new value breaks any client that switches on the response, which is most of them.
That asymmetry is the one to internalise: widening what you accept is safe, widening what you emit is not.
The mistake this prevents
The mistake is adding a required request field to an existing version because the new feature needs it. Every existing caller starts failing validation immediately. Add it optional, or add a new version path.
Takeaway
Additions to what you accept are safe; anything you emit or require is breaking. New required fields and new returned enum values both need a new version.
