惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

MongoDB | Blog
MongoDB | Blog
Recorded Future
Recorded Future
Jina AI
Jina AI
The Register - Security
The Register - Security
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
月光博客
月光博客
博客园 - 三生石上(FineUI控件)
F
Fortinet All Blogs
人人都是产品经理
人人都是产品经理
S
SegmentFault 最新的问题
Apple Machine Learning Research
Apple Machine Learning Research
L
LangChain Blog
Y
Y Combinator Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
GbyAI
GbyAI
The GitHub Blog
The GitHub Blog
Vercel News
Vercel News
博客园 - 【当耐特】
雷峰网
雷峰网
The Cloudflare Blog
阮一峰的网络日志
阮一峰的网络日志
aimingoo的专栏
aimingoo的专栏
云风的 BLOG
云风的 BLOG
I
InfoQ
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Google DeepMind News
Google DeepMind News
Security Latest
Security Latest
有赞技术团队
有赞技术团队
L
Lohrmann on Cybersecurity
P
Proofpoint News Feed
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
The Last Watchdog
The Last Watchdog
P
Privacy & Cybersecurity Law Blog
Scott Helme
Scott Helme
Google Online Security Blog
Google Online Security Blog
WordPress大学
WordPress大学
Hacker News - Newest:
Hacker News - Newest: "LLM"
NISL@THU
NISL@THU
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
B
Blog RSS Feed
Cyberwarzone
Cyberwarzone
K
Kaspersky official blog
F
Full Disclosure
Martin Fowler
Martin Fowler
Spread Privacy
Spread Privacy
D
Docker
C
Cisco Blogs
www.infosecurity-magazine.com
www.infosecurity-magazine.com
H
Hacker News: Front Page

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
The Version That Broke Everything
Prem Kapadne · 2026-06-20 · via DEV Community

A deep dive into API versioning — what goes wrong without it, the old way developers survived it, and why Spring 7's new approach changes everything.

⏱ 12 min read · 🎯 Intermediate · 🛠 Spring Boot 3.x / Spring 7+


It was a Tuesday. Nothing seemed wrong.

Imagine a dev team at a fast-growing startup. They have a food delivery platform — FoodRush — serving over a million mobile users, 5,000 restaurant partners, and 50 third-party business integrations. The backend is humming. The original restaurant menu API has been rock-solid for two years:

GET https://api.foodrush.com/restaurants/menu/{restaurant_id}

// Response looked like this — clean, simple
{
  "restaurant": "Pizza Palace",
  "items": [
    {
      "id": 101,
      "name": "Margherita Pizza",
      "price": 12.99,
      "available": true
    }
  ]
}

Then the product team had "improvements." They wanted richer data — restaurant ratings, nested pricing with currency info, prep times. A well-meaning developer updated the API. One deployment. No flags. No versioning. The new response looked like this:

💥 BREAKING CHANGE

{
  "restaurant": {           // was a string, now an object
    "name": "Pizza Palace",
    "id": "rest_001",
    "rating": 4.5
  },
  "menu_items": [            // "items" renamed to "menu_items"
    {
      "id": 101,
      "name": "Margherita Pizza",
      "pricing": {            // flat "price" replaced with nested object
        "amount": 12.99,
        "currency": "USD"
      },
      "availability": {       // boolean → object
        "in_stock": true,
        "prep_time": 20
      }
    }
  ]
}

The next morning: Slack explodes. The iOS app is crashing on launch. The restaurant partner dashboard is showing blank menus. Three enterprise integrations are throwing 500 errors. Support tickets are flooding in. The on-call engineer stares at the screen. The API "works" — it returns 200. But every single client is broken, because they were all parsing response.items and response.restaurant as a string. Now those fields don't exist in the old shape anymore.

Four breaking changes. Zero warning. Every client — mobile app, website, restaurant apps, delivery partner apps, third-party integrations — broke simultaneously. What should have been a simple product improvement turned into a company-wide incident. And it was entirely preventable.

The Real Problem
Evolving an API is inevitable. Products grow, requirements change, better designs emerge. The sin isn't changing your API — it's changing it without giving your clients a safe path through the transition.


What API versioning actually means

API versioning is not complicated in concept. It's the practice of allowing multiple versions of your API to exist simultaneously, so old clients keep working while new clients adopt the improved contract. Think of it like a software release: you don't delete version 1 the moment version 2 ships. Both live side by side until every client has migrated.

In the FoodRush disaster, the fix would have been simple. Instead of silently replacing the response shape, they should have done this:

// v1 — still returns the original format. Zero disruption.
GET https://api.foodrush.com/v1/restaurants/menu/{restaurant_id}

// v2 — new format for clients ready to upgrade
GET https://api.foodrush.com/v2/restaurants/menu/{restaurant_id}

With this in place, the migration becomes gradual and controlled:

  • Week 1 — Launch v2: v1 still works with zero disruption. v2 is available with the new format. Email sent to all developer partners about v2.
  • Month 1 — Mobile migration: New app version uses v2. Old app versions still work on v1. Users update at their own pace.
  • Month 2 — Partner migration: Partners test v2 in staging, switch when ready. No forced deadlines.
  • Month 6 — Deprecation notice: "v1 will be deprecated in 6 months." 99% of traffic on v2. Stragglers helped to migrate.
  • Year 1 — Sunset v1: Turn off v1 with confidence. All clients migrated. Zero business disruption.

That's the power of versioning: you control the pace of change. Clients don't get surprised. Your API evolves without breaking the trust of people building on top of it.


The four ways to version an API

Before we look at code, it's worth understanding the conceptual approaches. Every versioning strategy is just answering one question differently: how does the client tell the server which version it wants?

Approach Example
🛣️ URI Path /api/v1/users
🔍 Query Param /api/users?version=1
📋 Header X-API-VERSION: 1
🎭 Media Type Accept: application/vnd.myapp.v1+json

That last one — Media Type Versioning — is the most REST-compliant but also the least understood. Quick explainer: MIME stands for Multipurpose Internet Mail Extensions. It's the standard way to describe what type of data is being sent. The string application/vnd.myapp.v1+json breaks down as:

Part Meaning
application Top-level MIME type — general category of data
vnd Vendor-specific — this is a custom type, not a global standard
myapp Your application or company name
v1 The API version you're requesting
+json The data format — return it as JSON

The vnd prefix signals that this media type belongs to you — it's specific to your application, not a global internet standard. GitHub, for example, uses application/vnd.github.v3+json. The URL stays clean and stable; only the representation changes.


The old way — and why it hurts

Before Spring 7, developers had to wire up versioning manually inside their controllers. The mapping annotations — @GetMapping, @PostMapping etc. — carried the full versioning burden. Let's look at all four approaches in the legacy style:

1. URI path versioning (legacy)

@GetMapping({"", "/", "/v1"})
public String defaultPathVersion() {
    return "Response from API 1.0.0";
}

@GetMapping("/v2")
public String pathV2Version() {
    return "Response from API 2.0.0";
}

2. Request parameter versioning (legacy)

@GetMapping(params = "version=1")
public String defaultReqParamVersion() {
    return "Response from API 1.0.0";
}

@GetMapping(params = "version=2")
public String v2ReqParamVersion() {
    return "Response from API 2.0.0";
}

3. Request header versioning (legacy)

@GetMapping(headers = "X-API-VERSION=1")
public String defaultReqHeaderVersion() {
    return "Response from API 1.0.0";
}

@GetMapping(headers = "X-API-VERSION=2")
public String v2ReqHeaderVersion() {
    return "Response from API 2.0.0";
}

4. Media type versioning (legacy)

@GetMapping(produces = "application/vnd.eazyapp.v1+json")
public String defaultMediaTypeVersion() {
    return "Response from API 1.0.0";
}

@GetMapping(produces = "application/vnd.eazyapp.v2+json")
public String v2MediaTypeVersion() {
    return "Response from API 2.0.0";
}

At first glance these look fine. But imagine you have 50 endpoints across 10 controllers. Now imagine you need to add v3. Here's what the pain feels like:

  • 😵 Version logic lives in the wrong place. Your controller's job is to handle business logic — not to know which HTTP header a client passes. Versioning concerns bleed into every single method.
  • 🔁 Maintenance overhead explodes. Adding a new version means touching every controller, every method. In a large codebase that's hundreds of changes — all error-prone, all manual.
  • 🤯 No central source of truth. Want to know which versions your API supports? You have to grep through every annotation in every controller. There's no single configuration file that tells you.
  • 📦 Version sprawl becomes unmanageable. Three versions in, your controllers look like an archaeological dig — layers of v1, v2, v3 methods stacked on top of each other with no clear separation.
  • 🔀 Switching strategies is painful. If you started with URI versioning and later decided header versioning is cleaner, you'd need to rewrite annotations across your entire codebase.

⚠️ The hidden cost
The legacy approach technically works. But it doesn't scale. Every version you add multiplies the maintenance burden. By version 4, your codebase has become a versioning museum rather than a product.


The Spring 7 way — one config to rule them all

Spring 7 introduced a first-class API versioning system that flips the entire model. Instead of embedding version logic inside annotations scattered across your controllers, you declare your versioning strategy once in a central configuration class. The controllers themselves stay clean — they only use a simple version attribute on their mapping annotations.

The core idea: your controllers declare which version they serve. A separate configuration class declares how versioning works (URI, header, query param, media type). This separation of concerns is what makes it powerful.

1. URI path versioning (Spring 7+)

// Controller — declares versions, knows nothing about URL structure
@RestController
@RequestMapping("/api/versions/{v}")
public class VersionController {

    @GetMapping(version = "1.0")
    public String defaultVersion() {
        return "Response from API 1.0";
    }

    @GetMapping(version = "2.0+")  // "+" means 2.0 and above
    public String v2Version() {
        return "Response from API 2.0";
    }
}

// Central config — the only place versioning strategy lives
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void configureApiVersioning(ApiVersionConfigurer configurer) {
        configurer
            .usePathSegment(2)  // pick segment 2 of the URL path for version
            .addSupportedVersions("1.0", "2.0", "3.0");
    }
}

2. Request parameter versioning (Spring 7+)

// Controller stays identical — same version attributes
@RestController
@RequestMapping("/api/versions")
public class VersionController {

    @GetMapping(version = "1.0")
    public String defaultVersion() { ... }

    @GetMapping(version = "2.0+")
    public String v2Version() { ... }
}

// Only the config changes — the controller doesn't need to know
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void configureApiVersioning(ApiVersionConfigurer configurer) {
        configurer
            .useQueryParam("version")  // ?version=1.0 in the URL
            .addSupportedVersions("1.0", "2.0", "3.0")
            .setDefaultVersion("1.0");  // fallback when no version specified
    }
}

3. Request header versioning (Spring 7+)

// Controller — unchanged. Same version attributes, always.
@RestController
@RequestMapping("/api/versions")
public class VersionController {

    @GetMapping(version = "1.0")
    public String defaultVersion() { ... }

    @GetMapping(version = "2.0+")
    public String v2Version() { ... }
}

// Config: clients now pass X-API-VERSION: 1.0 in request headers
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void configureApiVersioning(ApiVersionConfigurer configurer) {
        configurer
            .useRequestHeader("X-API-VERSION")
            .addSupportedVersions("1.0", "2.0", "3.0")
            .setDefaultVersion("1.0");
    }
}

4. Media type versioning (Spring 7+)

// Controller — identical to all the above. This is the beauty of it.
@RestController
@RequestMapping("/api/versions")
public class VersionController {

    @GetMapping(version = "1.0")
    public String defaultVersion() { ... }

    @GetMapping(version = "2.0+")
    public String v2Version() { ... }
}

// Client sends: Accept: application/vnd.eazyapp+json;v=1.0
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void configureApiVersioning(ApiVersionConfigurer configurer) {
        configurer
            .useMediaTypeParameter(
                MediaType.parseMediaType("application/vnd.eazyapp+json"),
                "v"
            )
            .addSupportedVersions("1.0", "2.0", "3.0")
            .setDefaultVersion("1.0");
    }
}

Notice something remarkable: the controller code is identical across all four strategies. Only the WebConfig changes. Want to switch from URI versioning to header versioning across your entire application? Change one method in one file. Done.

What Spring 7 gives you
One config file = one source of truth for your entire versioning strategy. Your controllers are clean. Switching strategies costs you exactly one edit. Adding a new supported version is a single method call. This is what "separation of concerns" looks like in practice.


Legacy vs Spring 7 — side by side

Concern Legacy approach Spring 7+
Version declaration Inside every @GetMapping annotation Single version attribute per method
Strategy config Scattered Centralised
Switch strategy Rewrite every controller annotation Change one line in WebConfig
Add new version Manually add method per endpoint .addSupportedVersions("3.0")
Default version Manual fallback logic .setDefaultVersion()
Version range (2.0+) Not possible Supported natively
Supported in All Spring Boot versions Spring 7+ / Spring Boot 4+

Which strategy should you choose?

Strategy Best for Watch out for
URI Path /v1/ Public APIs, easy testing in browser, most widely understood URL proliferation as versions grow
Query Param ?version=1 Internal APIs, quick prototyping Cache complications — same URL, different responses
Header X-API-VERSION Clean URLs, microservice-to-microservice calls Harder to test without proper tools like Postman
Media Type Strict REST compliance, enterprise APIs Steeper learning curve, verbose Accept headers

For most projects — especially public-facing APIs and portfolio projects — URI path versioning is the pragmatic choice. It's explicit, easy to test, simple to document, and universally understood by developers of all experience levels. Move to header or media type versioning only when clean URLs and strict REST purity become actual requirements.

💡 Interview insight
In interviews, if asked about API versioning, most candidates mention URI versioning and stop there. Stand out by explaining all four strategies, their trade-offs, and then discussing the Spring 7 centralised configuration model. It shows you understand not just what to do but why the new approach is a better design.


The takeaway

API versioning is one of those things that feels optional until the day it isn't. The FoodRush disaster was fictional, but the scenario plays out in real companies every year. A well-intentioned improvement, deployed without versioning, breaks clients silently and erodes the trust of everyone building on top of your API.

The legacy Spring approach gave developers the tools to version APIs but left the housekeeping entirely up to them — scattered across annotations, mixed into business logic, painful to change. Spring 7's configureApiVersioning method finally gives versioning a proper home: one place, one strategy, one source of truth.

Whatever strategy you pick — URI, header, query param, or media type — the discipline of versioning your API from day one is the real lesson. Change is inevitable. How gracefully your clients survive that change is entirely in your hands.


Written as part of a Spring Boot deep dive. Code examples use Spring 7+ / Spring Boot 4.x with the new ApiVersionConfigurer API. Legacy examples are compatible with Spring Boot 2.x and 3.x.