Skip to content

Calendar Versioning (CalVer) for APIs — Date-Based Release Strategy

DodaTech Updated 2026-06-28 4 min read

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

Calendar Versioning (CalVer) uses release dates as version numbers, communicating release cadence clearly while requiring additional compatibility documentation.

What You'll Learn

By the end of this lesson, you will implement CalVer version formats, compare CalVer with SemVer, communicate compatibility alongside date-based versions, and choose when CalVer is appropriate.

Why It Matters

CalVer is ideal for APIs with regular release cadences where consumers track releases by date rather than compatibility semantics.

CalVer Format Comparison

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

class CalVerParser:
    FORMATS = {
        "YYYY": r"(\d{4})",
        "YYYY.MM": r"(\d{4})\.(\d{2})",
        "YYYY.MM.DD": r"(\d{4})\.(\d{2})\.(\d{2})",
        "YY.MM": r"(\d{2})\.(\d{2})",
    }

    def __init__(self, format_str: str = "YYYY.MM"):
        self.format_str = format_str
        self.pattern = self.FORMATS.get(format_str)

    def parse(self, version: str) -> Optional[Dict]:
        match = re.match(self.pattern, version)
        if not match:
            return None
        groups = match.groups()
        parts = self.format_str.split(".")
        result = {}
        for i, part in enumerate(parts):
            if i < len(groups):
                result[part.lower()] = int(groups[i])
        return result

    def generate(self, dt: datetime = None) -> str:
        dt = dt or datetime.utcnow()
        parts = self.format_str.split(".")
        values = []
        for p in parts:
            if p == "YYYY": values.append(str(dt.year))
            elif p == "YY": values.append(str(dt.year % 100).zfill(2))
            elif p == "MM": values.append(str(dt.month).zfill(2))
            elif p == "DD": values.append(str(dt.day).zfill(2))
        return ".".join(values)

    def compare(self, v1: str, v2: str) -> int:
        p1 = self.parse(v1)
        p2 = self.parse(v2)
        if not p1 or not p2:
            return 0
        for key in ["year", "month", "day"]:
            if key in p1 and key in p2:
                if p1[key] != p2[key]:
                    return -1 if p1[key] < p2[key] else 1
        return 0

calver = CalVerParser("YYYY.MM")
v = calver.generate(datetime(2026, 6, 28))
print(f"Generated: {v}")
parsed = calver.parse("2026.06")
print(f"Parsed: {parsed}")
cmp = calver.compare("2026.06", "2026.12")
print(f"Compare: {cmp}")

CalVer vs SemVer Decision

class VersionSchemeDecider:
    def __init__(self):
        self.factors = {}

    def add_factor(self, name: str,
                   calver_score: int,
                   semver_score: int):
        self.factors[name] = {
            "calver": calver_score,
            "semver": semver_score,
        }

    def recommend(self) -> str:
        calver_total = sum(f["calver"] for f in self.factors.values())
        semver_total = sum(f["semver"] for f in self.factors.values())
        return "calver" if calver_total > semver_total else "semver"

    def get_recommendation_reason(self) -> str:
        rec = self.recommend()
        reasons = []
        for name, scores in self.factors.items():
            pref = "calver" if scores["calver"] > scores["semver"] else "semver"
            if pref == rec:
                reasons.append(name)
        return f"Recommended: {rec}. Key factors: {', '.join(reasons)}"

decider = VersionSchemeDecider()
decider.add_factor("Release cadence", 5, 2)
decider.add_factor("Compatibility communication", 2, 5)
decider.add_factor("Consumer type", 3, 4)
print(decider.get_recommendation_reason())

CalVer API Router

class CalVerRouter:
    def __init__(self):
        self.releases: Dict[str, Dict] = {}
        self.latest: Optional[str] = None

    def add_release(self, date_str: str,
                    handler: callable,
                    breaking: bool = False):
        self.releases[date_str] = {
            "handler": handler,
            "breaking": breaking,
        }
        if not self.latest or date_str > self.latest:
            self.latest = date_str

    def route(self, version: str,
              request: Dict) -> Dict:
        release = self.releases.get(version)
        if not release:
            return {"status": 404,
                    "body": {"error": f"Version {version} not found"}}
        return release["handler"](request)

    def get_breaking_changes_since(self,
                                    version: str) -> List[str]:
        changes = []
        for date_str, release in sorted(self.releases.items()):
            if date_str > version and release["breaking"]:
                changes.append(date_str)
        return changes

router = CalVerRouter()
router.add_release("2026.01", lambda r: {"version": "2026.01"})
router.add_release("2026.06", lambda r: {"version": "2026.06"},
                   breaking=True)
breaking = router.get_breaking_changes_since("2026.01")
print(f"Breaking changes since 2026.01: {breaking}")

Common Mistakes

Mistake 1: No Compatibility Information

CalVer does not communicate compatibility. Always document breaking changes alongside the version.

Mistake 2: Overly Precise Versions

YYYY.MM.DD is too granular for most APIs. YYYY.MM or YYYY.MM.MINOR provides a better balance.

Mistake 3: Non-Standard Date Formats

Use ISO 8601-based formats (YYYY.MM) for consistency and correct alphabetical sorting.

Mistake 4: Assuming Chronological Order

Versions like 2026.1 (January) and 2026.12 (December) sort correctly as strings. Test version sorting.

Mistake 5: Mixing CalVer and SemVer

Do not use both schemes simultaneously. Choose one and communicate it clearly.

Practice Questions

  1. What is the CalVer format and when is it appropriate?
  2. How does CalVer communicate breaking changes?
  3. What is the difference between YYYY.MM and YYYY.MM.DD CalVer?
  4. How do you compare two CalVer versions?
  5. When should you choose CalVer over SemVer?

Challenge

Build a CalVer-based API Versioning system that generates versions in YYYY.MM format, routes requests to the correct handler, tracks breaking changes per release, and provides a compatibility report between any two versions.

FAQ

What is Calendar Versioning (CalVer)?

CalVer uses release dates as version numbers, such as 2026.06 for a June 2026 release. It prioritizes release timing over compatibility semantics.

How does CalVer communicate breaking changes?

CalVer does not inherently indicate breaking changes. You must document breaking changes separately through changelogs, deprecation headers, or companion SemVer tags.

Can CalVer versions be sorted chronologically?

Yes, if formatted as YYYY.MM with zero-padded months. String sorting matches chronological order.

What is the difference between CalVer and SemVer?

CalVer communicates release timing. SemVer communicates compatibility. SemVer tells consumers if an upgrade is safe; CalVer tells them when it was released.

When should I use CalVer?

Use CalVer for APIs with frequent releases, internal APIs where consumers track releases by date, or when release timing is more important than compatibility guarantees.

Mini Project

Build a CalVer-based versioning system that generates YYYY.MM version strings, supports routing per release, tracks breaking changes with dates, provides compatibility reports between releases, and compares CalVer and SemVer for the same API.

What's Next

Learn about Version Formats for choosing the right version scheme, or explore Semantic Versioning for compatibility-based versioning.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro