Breaking Changes in APIs — Identification, Communication, and Migration
In this tutorial, you'll learn about Breaking Changes. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
Breaking changes are modifications to your API that require consumers to update their code. Managing them well is essential for maintaining consumer trust.
What You'll Learn
By the end of this lesson, you will identify breaking changes, communicate them effectively, create Migration timelines, use feature flags for gradual rollout, and implement version coexistence.
Why It Matters
Unexpected breaking changes erode consumer trust and create maintenance burden. Planned, communicated breaking changes are manageable and expected.
Real-World Use
Durga Antivirus Pro announces breaking changes 6 months in advance via email, documentation banners, and API response headers, with automated migration tools provided.
Breaking Change Categories
flowchart TD
Breaking[Breaking Changes]-->Contract[Contract Changes]
Breaking-->Behavior[Behavior Changes]
Breaking-->Infra[Infrastructure Changes]
Contract-->Remove[Remove endpoint/field]
Contract-->Rename[Rename field]
Contract-->Type[Change field type]
Contract-->Required[Add required field]
Behavior-->Semantics[Change response meaning]
Behavior-->Pagination[Change pagination]
Infra-->Auth[Change auth requirements]
Infra-->Rate[Change rate limits]
Breaking Change Detector
Detect breaking changes between API versions.
from typing import Dict, List, Optional, Set
from enum import Enum
class BreakingLevel(Enum):
NONE = "none"
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
class BreakingChange:
def __init__(self, category: str,
description: str,
level: BreakingLevel,
affected_endpoints: List[str],
migration_guide: str = ""):
self.category = category
self.description = description
self.level = level
self.affected_endpoints = affected_endpoints
self.migration_guide = migration_guide
class ChangeDetector:
def __init__(self):
self.changes: List[BreakingChange] = []
def compare_endpoints(self, old: List[Dict],
new: List[Dict]):
old_endpoints = {e["path"]: e for e in old}
new_endpoints = {e["path"]: e for e in new}
for path in old_endpoints:
if path not in new_endpoints:
self.changes.append(BreakingChange(
"contract",
f"Endpoint removed: {path}",
BreakingLevel.HIGH,
[path],
f"Use replacement endpoint"
))
for path in new_endpoints:
if path not in old_endpoints:
pass
for path in set(old_endpoints) & set(new_endpoints):
old_e = old_endpoints[path]
new_e = new_endpoints[path]
self._compare_methods(path, old_e, new_e)
self._compare_params(path, old_e, new_e)
def _compare_methods(self, path: str,
old: Dict, new: Dict):
old_methods = set(old.get("methods", []))
new_methods = set(new.get("methods", []))
removed = old_methods - new_methods
for method in removed:
self.changes.append(BreakingChange(
"contract", f"Method {method} removed "
f"from {path}", BreakingLevel.HIGH, [path]
))
def _compare_params(self, path: str,
old: Dict, new: Dict):
old_params = {p["name"]: p
for p in old.get("params", [])}
new_params = {p["name"]: p
for p in new.get("params", [])}
for name, op in old_params.items():
if name in new_params:
np = new_params[name]
if op.get("type") != np.get("type"):
self.changes.append(BreakingChange(
"contract",
f"Param '{name}' type changed "
f"in {path}",
BreakingLevel.MEDIUM, [path]
))
if op.get("required") and not np.get("required"):
pass
if not op.get("required") and np.get("required"):
self.changes.append(BreakingChange(
"contract",
f"Param '{name}' became required "
f"in {path}",
BreakingLevel.HIGH, [path]
))
def get_breaking_changes(self) -> List[BreakingChange]:
return [c for c in self.changes
if c.level in (BreakingLevel.MEDIUM,
BreakingLevel.HIGH)]
detector = ChangeDetector()
detector.compare_endpoints(
[{"path": "/api/v1/scan", "methods": ["GET", "POST"],
"params": [{"name": "format", "type": "string"}]}],
[{"path": "/api/v1/scan", "methods": ["GET"],
"params": [{"name": "format", "type": "int"}]}]
)
for change in detector.get_breaking_changes():
print(f"Breaking: {change.description} "
f"({change.level.value})")
Communication Template
Communicate breaking changes to consumers effectively.
from typing import Dict, List, Optional
from datetime import datetime
class BreakingChangeAnnouncement:
def __init__(self, title: str,
version_from: str,
version_to: str,
date: datetime):
self.title = title
self.version_from = version_from
self.version_to = version_to
self.date = date
self.changes: List[Dict] = []
self.migration_deadline: Optional[datetime] = None
def add_change(self, description: str,
impact: str,
migration_steps: List[str]):
self.changes.append({
"description": description,
"impact": impact,
"migration_steps": migration_steps,
})
def set_deadline(self, deadline: datetime):
self.migration_deadline = deadline
def generate_html(self) -> str:
html = f"<h2>{self.title}</h2>\n"
html += f"<p>Version {self.version_from} to "
html += f"{self.version_to}</p>\n"
html += f"<p>Announced: {self.date.date()}</p>\n"
if self.migration_deadline:
html += f"<p>Migration deadline: "
html += f"{self.migration_deadline.date()}</p>\n"
html += "<h3>Changes</h3><ul>\n"
for change in self.changes:
html += f"<li><strong>{change['description']}"
html += f"</strong> - {change['impact']}</li>\n"
html += "</ul>\n"
return html
def generate_email_subject(self) -> str:
return (f"[URGENT] Breaking API Changes: "
f"{self.version_from} to {self.version_to}")
announcement = BreakingChangeAnnouncement(
"API v2 Breaking Changes",
"v2", "v3",
datetime(2026, 6, 28)
)
announcement.add_change(
"Removed deprecated status field",
"All consumers using status field must migrate to state field",
["Replace status with state in request bodies",
"Update response parsing to use state field"]
)
announcement.set_deadline(datetime(2026, 12, 31))
print(announcement.generate_html()[:200] + "...")
Feature Flag Migration
Use feature flags to gradually roll out breaking changes.
from typing import Dict, Optional, Set
import time
class FeatureFlag:
def __init__(self, name: str,
enabled_for: Optional[Set[str]] = None):
self.name = name
self.enabled_for = enabled_for or set()
self.global_enabled = False
self.enable_percent = 0
def enable_for_all(self):
self.global_enabled = True
def enable_for_client(self, client_id: str):
self.enabled_for.add(client_id)
def is_enabled(self, client_id: str) -> bool:
if self.global_enabled:
return True
if client_id in self.enabled_for:
return True
return False
class FlaggedMigration:
def __init__(self):
self.flags: Dict[str, FeatureFlag] = {}
def create_flag(self, name: str):
self.flags[name] = FeatureFlag(name)
def enable_for_client(self, flag_name: str,
client_id: str):
flag = self.flags.get(flag_name)
if flag:
flag.enable_for_client(client_id)
def get_version_for_client(
self, client_id: str, flag_name: str
) -> str:
flag = self.flags.get(flag_name)
if flag and flag.is_enabled(client_id):
return "new"
return "old"
def migrate_client(self, client_id: str,
flag_name: str):
self.enable_for_client(flag_name, client_id)
migration = FlaggedMigration()
migration.create_flag("new-auth-format")
migration.migrate_client("partner-1", "new-auth-format")
version = migration.get_version_for_client(
"partner-1", "new-auth-format"
)
print(f"Partner-1 using: {version} version")
Common Mistakes
Mistake 1: Silent Breaking Changes
Changing behavior without announcing it breaks consumers silently. Always communicate changes proactively.
Mistake 2: No Migration Period
Consumers need time to migrate. Provide at least 3-6 months between announcement and enforcement.
Mistake 3: Breaking Changes in Patch Releases
Breaking changes belong in major versions. Never break consumers in minor or patch releases.
Mistake 4: No Migration Tools
Provide example code, migration scripts, or compatibility libraries to reduce consumer effort.
Mistake 5: Ignoring Deprecation Headers
Use Deprecation and Sunset HTTP headers to communicate deprecation status programmatically.
Practice Questions
- What qualifies as a breaking change?
- How do you communicate breaking changes to consumers?
- What is a reasonable migration period for breaking changes?
- How do feature flags help with breaking change rollout?
- What HTTP headers indicate API deprecation?
Challenge
Build a breaking change management system that detects changes between OpenAPI specs, generates consumer announcements with migration guides, tracks which consumers have migrated, and enforces a 6-month deprecation window.
FAQ
Mini Project
Build a breaking change management system that detects changes between OpenAPI specs, classifies them as breaking or non-breaking, generates formatted announcements with migration timelines, sends deprecation headers in API responses, and tracks consumer migration progress.
What's Next
Learn about Deprecation and Sunset Headers for programmatic deprecation communication, or explore Migration Guides for consumer transition support.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro