Skip to content

Breaking Changes in APIs — Identification, Communication, and Migration

DodaTech Updated 2026-06-28 6 min read

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

  1. What qualifies as a breaking change?
  2. How do you communicate breaking changes to consumers?
  3. What is a reasonable migration period for breaking changes?
  4. How do feature flags help with breaking change rollout?
  5. 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

What is a breaking change?

A breaking change is any API modification that requires consumers to update their code to continue working. Examples include removing fields, changing types, or adding required parameters.

How do I announce breaking changes?

Use multiple channels: email to registered developers, documentation banners, API response headers (Deprecation, Sunset), and changelog entries.

Can I make breaking changes in the same version?

No. Breaking changes require a new major version. Use the existing version for backward-compatible changes only.

What if a consumer does not migrate before the deadline?

The old version stops working after the sunset date. Monitor usage and send reminders before the deadline.

How do feature flags help with breaking changes?

Feature flags let you enable the new behavior for specific clients, test it in production, and gradually roll it out before making it default.

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