Skip to content

Backward Compatibility — Maintaining API Stability Across Versions

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you'll learn about Backward Compat. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

Backward compatibility means that consumers written for an older API version continue to work with a newer version without modification.

What You'll Learn

By the end of this lesson, you will implement backward-compatible changes, deprecate fields properly, use tolerance strategies for input validation, and write compatibility tests.

Why It Matters

Breaking consumers with every API update destroys trust. Backward compatibility allows your API to evolve while maintaining a stable contract for existing consumers.

Real-World Use

Durga Antivirus Pro maintains backward compatibility by adding new fields to responses while keeping existing fields unchanged, and deprecating fields two releases before removal.

Compatibility Rules

flowchart TD
    Change[API Change]-->Compat{Backward Compatible?}
    Compat-->|Add Field|Yes[Safe - Additive]
    Compat-->|Rename Field|No[Breaking]
    Compat-->|Remove Field|No2[Breaking]
    Compat-->|Change Type|No3[Breaking]
    Compat-->|Make Optional Req|No4[Breaking]
    Compat-->|Make Req Optional|Yes2[Safe - Loosening]

Compatibility Checker

Check if API changes are backward compatible.

from typing import Dict, List, Optional, Any
from enum import Enum

class CompatLevel(Enum):
    FULLY_COMPATIBLE = "fully_compatible"
    COMPATIBLE_WITH_DEFAULTS = "compatible_with_defaults"
    BREAKING = "breaking"

class CompatChecker:
    def __init__(self, old_schema: Dict,
                 new_schema: Dict):
        self.old_schema = old_schema
        self.new_schema = new_schema
        self.issues: List[str] = []

    def check_response_schema(self) -> CompatLevel:
        old_fields = self.old_schema.get("fields", {})
        new_fields = self.new_schema.get("fields", {})

        for field_name, old_def in old_fields.items():
            if field_name not in new_fields:
                self.issues.append(
                    f"Field '{field_name}' removed"
                )
                continue
            new_def = new_fields[field_name]
            if old_def.get("type") != new_def.get("type"):
                self.issues.append(
                    f"Field '{field_name}' type changed "
                    f"from {old_def['type']} to {new_def['type']}"
                )
            if not old_def.get("optional") and \
                    new_def.get("optional"):
                self.issues.append(
                    f"Field '{field_name}' changed "
                    f"from required to optional"
                )

        if self.issues:
            return CompatLevel.BREAKING
        return CompatLevel.FULLY_COMPATIBLE

    def check_request_schema(self) -> CompatLevel:
        old_fields = self.old_schema.get("fields", {})
        new_fields = self.new_schema.get("fields", {})

        for field_name, new_def in new_fields.items():
            if field_name not in old_fields:
                if not new_def.get("optional"):
                    self.issues.append(
                        f"New required field '{field_name}' added"
                    )
            else:
                old_def = old_fields[field_name]
                if old_def.get("type") != new_def.get("type"):
                    self.issues.append(
                        f"Field '{field_name}' type changed"
                    )
                if not new_def.get("optional") and \
                        old_def.get("optional"):
                    self.issues.append(
                        f"Field '{field_name}' became required"
                    )

        if self.issues:
            return CompatLevel.BREAKING
        return CompatLevel.FULLY_COMPATIBLE

old = {"fields": {"name": {"type": "string", "optional": False}}}
new = {"fields": {"name": {"type": "string", "optional": False},
                   "email": {"type": "string", "optional": True}}}
checker = CompatChecker(old, new)
print(f"Response compat: {checker.check_response_schema().value}")
print(f"Request compat: {checker.check_request_schema().value}")

Field Deprecation Strategy

Properly deprecate fields before removing them.

from typing import Dict, Optional, List
from datetime import datetime, timedelta

class DeprecationTracker:
    def __init__(self, api_version: str):
        self.api_version = api_version
        self.deprecated: Dict[str, Dict] = {}

    def mark_deprecated(self, field: str,
                        endpoint: str,
                        removed_in_version: str,
                        alternative: str = ""):
        self.deprecated[f"{endpoint}:{field}"] = {
            "field": field,
            "endpoint": endpoint,
            "deprecated_in": self.api_version,
            "removed_in": removed_in_version,
            "alternative": alternative,
            "deprecated_at": datetime.utcnow(),
        }

    def add_deprecation_header(self, endpoint: str,
                               response_headers: Dict
                               ) -> Dict:
        endpoint_deprecated = [
            d for key, d in self.deprecated.items()
            if key.startswith(endpoint)
        ]
        if endpoint_deprecated:
            response_headers["Deprecation"] = "true"
            response_headers["Sunset"] = \
                endpoint_deprecated[0].get("removed_in", "")
        return response_headers

    def get_active_deprecations(self) -> List[Dict]:
        return [
            d for d in self.deprecated.values()
        ]

    def should_remove(self, field: str, endpoint: str,
                      current_version: str) -> bool:
        key = f"{endpoint}:{field}"
        dep = self.deprecated.get(key)
        if not dep:
            return False
        return dep["removed_in"] <= current_version

tracker = DeprecationTracker("v2")
tracker.mark_deprecated("old_status", "/api/scan",
                        "v3", alternative="use status field")
headers = tracker.add_deprecation_header(
    "/api/scan", {}
)
print(f"Deprecation headers: {headers}")

Tolerance Strategies

Build tolerance into input validation for backward compatibility.

from typing import Dict, Any, Optional, List

class TolerantParser:
    def __init__(self, tolerance_level: str = "strict"):
        self.tolerance_level = tolerance_level

    def parse_field(self, value: Any,
                    expected_type: str,
                    field_name: str) -> Optional[Any]:
        if value is None:
            if self.tolerance_level == "strict":
                return None
            return self._default_for_type(expected_type)

        try:
            if expected_type == "int":
                return int(value)
            elif expected_type == "float":
                return float(value)
            elif expected_type == "bool":
                if isinstance(value, bool):
                    return value
                if value.lower() in ("true", "1", "yes"):
                    return True
                return False
            return value
        except (ValueError, TypeError):
            if self.tolerance_level == "tolerant":
                return self._default_for_type(expected_type)
            return None

    def _default_for_type(self, type_name: str) -> Any:
        defaults = {
            "int": 0, "float": 0.0, "bool": False,
            "string": "", "list": [], "dict": {},
        }
        return defaults.get(type_name, None)

    def parse_request_body(
        self, body: Dict, schema: Dict
    ) -> Dict:
        result = {}
        for field, config in schema.items():
            value = body.get(field)
            parsed = self.parse_field(
                value, config.get("type", "string"), field
            )
            if parsed is not None or field in body:
                result[field] = parsed
            elif config.get("required"):
                result[field] = self._default_for_type(
                    config.get("type", "string")
                )
        return result

parser = TolerantParser("tolerant")
result = parser.parse_request_body(
    {"count": "5", "active": "yes", "name": "test"},
    {"count": {"type": "int", "required": True},
     "active": {"type": "bool", "required": True},
     "name": {"type": "string", "required": True}}
)
print(f"Parsed tolerantly: {result}")

Common Mistakes

Mistake 1: Removing Fields Without Deprecation

Mark fields as deprecated with a Sunset header. Give consumers at least one full release cycle before removal.

Mistake 2: Changing Field Types

A field that was string becoming integer breaks all consumers. Create a new field instead.

Mistake 3: Making Optional Fields Required

Adding a new required field breaks all existing consumers. Always add new fields as optional.

Mistake 4: Strict Type Validation

Type coercion with tolerance allows consumers to pass older formats without breaking.

Mistake 5: Not Testing Compatibility

Without automated compatibility tests, breaking changes slip into releases. Test old clients against new APIs.

Practice Questions

  1. What is backward compatibility in APIs?
  2. What changes are always breaking?
  3. How do you deprecate a field properly?
  4. What is tolerance in input validation?
  5. How do you test backward compatibility?

Challenge

Build a backward compatibility checker that validates new API responses against old client expectations, flags breaking changes, generates a compatibility report, and suggests non-breaking alternatives for each change.

FAQ

What is backward compatibility?

Backward compatibility means consumers built for an older version of your API continue to work with a newer version without requiring changes.

Is adding a field backward compatible?

Adding an optional field to responses is backward compatible. Adding a required field to requests is breaking.

How long should deprecated fields remain?

Deprecate in one version, remove in the next. Give consumers at least 6 months or one full release cycle.

What is the tolerance pattern?

Tolerance means accepting multiple input formats for the same field. For example, accepting both string and number for a numeric field.

Can I rename a field compatibly?

No, renaming is breaking. Add the new field, deprecate the old one, keep both until removal.

Mini Project

Build a backward compatibility layer for an API that supports field aliasing (old name redirects to new name), tolerance Parsing (accepts multiple input formats), deprecation headers (Deprecation and Sunset headers), and automated compatibility testing against old client snapshots.

What's Next

Learn about Breaking Changes and how to manage them, or explore Semantic Versioning for version numbering.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro