Last week, I had the chance to attend Spring I/O 2025 in Barcelona, and two talks in particular really stuck with me. One was by Victor Rentea, who dove into the common pitfalls of API design. The other was by Rossen Stoyanchev, who explored API versioning and how it’s supported in the Spring Framework.
Both talks were insightful—but they also left me with a lingering question:
Should we version our APIs at all, or is it just a necessary evil?
As a backend developer, I’ve worked on a wide range of projects—RESTful and GraphQL APIs alike—and I’ve seen just about every versioning strategy under the sun. Some teams go all-in on semantic versioning, others avoid it entirely. And when it comes to upgrading endpoints or deprecating old ones, there’s rarely a clear path forward.
One of the biggest challenges? Clients. Some are modern and agile, ready to adopt new versions quickly. Others are legacy systems that are barely maintained—where “deployment” means restarting the app when it crashes in production.
So how do we navigate this mess?
Let’s break it down. In this post, I’ll walk through real-world examples, common strategies, and the trade-offs that come with each approach. Whether you’re building a greenfield API or maintaining a decade-old monolith, this is a conversation worth having.
Common ways of versioning
Let’s take a look at some examples of API versioning that where mentioned in the talk Rossen Stoyanchev.
Path variable versioning
One of the most common way of versioning an api is having a path variable containing the version of a request method.
/v1/users
/v1/users/:id
When we design our API we immediately add a version to it, meaning things will maybe change.
Next we update one of the endpoints to a new version
/v1/users
/v1/users/:id
/v2/users/:id
Let’s dissect what has happend here. First of all we don’t touch any of the existing methods in the name of backwards compatibility. Then we create a /v2 method with our breaking change, and that lives next to our v1 method.
Query parameter versioning
One of the more uncommon methods is using the query parameter versioning. This way of working works with a default version and a new version provided with a query parameter.
The default version 1 doesn’t use any parameters
/users
/users/:id
Now if we want to use version 2 ofusers/:id
we add the version query parameter
/users
/users/:id
/users/:id?version=2
This way we need to specify the latest version with query parameter in order to get the latest version. Without the query parameter you will get the previous version.
This in my opinion is difficult to sell, because the default version is the older version and not the new version. As developers we updated the version and that one should now be the default and not the old one. If we reverse it, then our clients will break
To break or not to break, that is the question - The concerned backend developer
The Implicit Contract of Versioning
When we design APIs, we almost instinctively slap a version number onto the URL—/v1
, /api/v1
, or something similar. It’s so common that we rarely stop to question it.
But that small detail says a lot.
By adding a version, we’re acknowledging—perhaps subconsciously—that our API isn’t final. We’re building with the expectation that it will evolve. That endpoints will change. And that some things might even break.
In a way, versioning is a signal to our clients: “Be ready for change.”
It tells consumers of our API that we’re thinking ahead, and that we’re designing with growth in mind. But it also opens the door to complexity—because once you introduce versions, you have to manage them.
So the question becomes: Are we versioning because we need to, or because we assume we should?
The Consumer challenge
When we introduce a new API version, we’re sending a clear message to our clients:
“Something is going to change. We still support the old version—for now—but we really want you to move forward.”
But here’s the tricky part: how do we actually communicate that?
It’s not like we can pop up a notification mid-request saying, “Hey, there’s a new version available!” Sure, we could include a response header with a deprecation warning or a link to the new version—but let’s be honest: if the client is a machine, it’s probably going to ignore it.
This is where things get nuanced. Not all clients are created equal, and how we handle versioning communication depends heavily on who is consuming our API:
- Internal team clients
These are the easiest to manage. You can walk over to their desk (or ping them) and coordinate version upgrades directly. - Enterprise clients within the same organization
Slightly more formal, but still manageable. You might need internal documentation, versioning policies, or even internal SLAs to ensure smooth transitions. - External clients (e.g., partners or customers)
These require clear communication channels—think changelogs, email notifications, API dashboards, and long deprecation timelines. - Public APIs
This is the most challenging scenario. You need robust documentation, clear versioning strategies, and a well-defined lifecycle for each version. Breaking changes must be handled with extreme care.
Each of these client types comes with its own expectations, constraints, and communication needs. And if we don’t tailor our versioning strategy accordingly, we risk breaking trust—or worse, breaking production systems.
Internal team clients: Keep It Simple, Keep It Synchronized
When your API consumer is part of your own team, versioning becomes a lot more manageable—and arguably, a lot more streamlined.
In this scenario, you don’t need to expose multiple versions of individual endpoints. Instead, you treat the entire application version as the single source of truth. The backend and the client are tightly coupled, and they evolve together. This means:
- You only maintain one version of the API at a time.
- The application version (e.g.,
1.2.0
) applies to both the backend and the client. - You can easily verify deployments by checking that both components are running the same version in the target environment.
This approach avoids the overhead of maintaining multiple API versions and keeps the development cycle clean and predictable.
Into Practice: A Versioned Workflow
Let’s walk through how this might look in a real-world development cycle:
- Initial Development
The team collaborates on the first iteration of the API. Together, they define the structure and behavior of the endpoints—essentially forming a contract between the backend and frontend (or other internal consumers). - Release Candidate
Once development and testing are complete, a release candidate is created and deployed. This version is now live and stable. - Next Iteration
A new feature or change requires an update to an existing endpoint. The team revisits the API contract, discusses the changes, and agrees on the new structure. - Contract Enforcement
If the contract is defined using tools like OpenAPI, Pact or Spring Cloud Contract, any breaking changes will trigger failing tests in both the backend and the client. This ensures that both sides are updated in sync before the next release. - Rollback Scenario
If something goes wrong in production and a rollback is needed, both the backend and the client must be rolled back together. This tight coupling requires coordination—but it also ensures consistency.
This model works best in environments where teams are co-located (or at least closely collaborating) and where deployments can be coordinated. It’s not suitable for public APIs or loosely coupled systems—but for internal development, it’s clean, efficient, and easy to manage.
Enterprise clients within the same organization
This scenario is similar to working with internal clients—but with one key difference: the consuming team is outside your immediate team, operating with its own roadmap, priorities, and timelines.
In large organizations, this is a common setup. And it introduces a new layer of complexity.
To manage this effectively, early alignment is critical. Before the release train even starts rolling, both teams should have already discussed the upcoming changes. Ideally, API contracts are exchanged and agreed upon in advance—but in reality, this often happens during the release cycle itself.
Regardless of timing, the requirements for the API must be clearly defined and validated. This means:
- The API contract should be treated as a shared responsibility.
- Any changes should be validated against real use cases from the consuming team.
- Both teams should be involved in end-to-end testing.
Once the requirements are met and both sides are confident in the integration, a release candidate can be built.
Just like in the internal team scenario, the application version remains the anchor point. The version of the backend application determines the version of the API that gets released. This keeps things consistent and traceable across environments.
This model works well when there’s a culture of collaboration and shared ownership. It requires more planning and communication than intra-team development—but it avoids the chaos of misaligned expectations and last-minute surprises.
Into Practice: Coordinating the Release Train
In large organizations, successful API delivery across teams hinges on planning, communication, and coordination. Here’s how that typically plays out in practice:
- Pre-Release Alignment
Before the release train begins, both teams come together to align on what needs to be delivered. This is the ideal time to define and exchange API contracts. Doing this early helps reduce miscommunication and sets clear expectations. - Planning Phase
During sprint or release planning, the teams agree on key milestones:- When the API will be ready for consumption
- When the consuming team can begin development or testing
- What dependencies or blockers need to be addressed
- Release Window
As the release train nears completion, both teams agree on a release window. A release candidate is built, and this is where end-to-end testing becomes critical. Both teams must validate the integration thoroughly before anything goes live. - Rollback Readiness
If something goes wrong in production, a rollback may be necessary. In this case, both the backend and the client must be rolled back together. This requires tight coordination and a shared understanding of rollback procedures.
This approach ensures that even when teams are working independently, they remain aligned on delivery goals and timelines. It’s not just about shipping code—it’s about delivering value without surprises.
External clients: Contracts, Commitments, and Compatibility
When your API is consumed by an external party—outside your organization—the stakes are higher. You’re no longer just managing internal expectations; you’re now bound by formal agreements, service-level expectations, and the need for long-term stability.
The first step is to verify the contract between both organizations. If the agreement includes a grace period between API versions, that becomes a non-negotiable part of your release strategy. These terms must be clearly defined and documented up front—ideally in a formal API usage agreement or SLA.
From there, the process mirrors internal cross-team collaboration, but with more structure and communication:
- Pre-release communication is essential. The external client must be notified well in advance that a new version is coming.
- Release planning should include a clear timeline for when the new version will be available and when the old version will be deprecated.
- Dual-version support becomes necessary. Unlike internal clients, you can’t assume external consumers will upgrade immediately. To honor the grace period, the previous version of the application must continue running alongside the new one.
This approach ensures that external consumers have the time and flexibility to adapt, without breaking their existing integrations.`
Into Practice: Supporting External Consumers with Graceful Versioning
When releasing a new version of your API for external clients, the process must be deliberate and transparent. Here’s how it typically unfolds:
- New Version Deployment
The development team builds and deploys a new version of the application. Crucially, this version is deployed alongside the existing one—not as a replacement. This ensures that external consumers have a grace period to migrate at their own pace. - Version Isolation
To clearly separate versions, the API is exposed using either DNS-based routing or global versioning within the application. This approach ensures that consumers can explicitly target the version they support, without ambiguity.
Example:
DNS based versioning
v1 -> https://api-v1.app.com
v2 -> https://api-v2.app.com
Global versioning
v1 -> https://api.app.com/v1
v2 -> https://api.app.com/v2
- Client Communication
Once the new version is live, the product manager and business stakeholders notify all external clients. This communication includes:- A clear announcement of the new version
- Documentation and migration guides
- The end date of the grace period for the previous version
- Grace Period Management
During the grace period, both versions remain operational. This gives clients time to test, adapt, and deploy their changes without disruption. - Sunsetting the Old Version
Once the grace period ends and all clients have migrated, the development team decommissions the old version. This step should be well-documented and communicated in advance to avoid surprises.
Public api: Stability, Scale, and Strategic Change
At the highest level of exposure, we find public APIs—interfaces consumed by a wide range of clients, from free-tier users to enterprise-grade paying customers. This is where versioning becomes not just a technical concern, but a product and business decision.
When planning changes to a public API, the first step is to assess the impact. Logging and usage analytics become essential tools here. They help answer questions like:
- How many clients are using the affected endpoints?
- Are those clients high-value customers?
- What’s the potential fallout if something breaks?
This data-driven approach helps determine whether a change is low-risk or requires a more cautious rollout.
In most cases, the grace period for migrating to a new version will be longer than with internal or even external enterprise clients. The exact duration often depends on the terms defined in client contracts, especially for paying customers who expect stability and predictability.
Operationally, the same principles used for external consumers still apply:
- Clear communication about upcoming changes
- Dual-version support during the transition period
- Strict adherence to deprecation timelines
But with public APIs, the scale and diversity of consumers mean you’ll need more robust tooling, documentation, and support processes in place.
Into Practice: Deploying and Managing Multiple API Versions
When delivering a public API—or any API with a wide consumer base—the development team must plan for graceful transitions between versions. That means running multiple versions of the application side by side in production to give consumers time to migrate.
Here’s how that typically works:
- New Version Deployment
The development team builds and deploys a new version of the application. This version is introduced alongside the existing one, not as a replacement. This ensures that existing consumers can continue using the old version while preparing to upgrade. - Versioning Strategy
To clearly separate versions, the API should expose version identifiers either at the DNS level or through global URI versioning. For example:
DNS based versioning
v1 -> https://api-v1.app.com
v2 -> https://api-v2.app.com
Global versioning
v1 -> https://api.app.com/v1
v2 -> https://api.app.com/v2
Both approaches are valid, and the choice depends on your infrastructure, deployment model, and how much isolation you want between versions.
- Documentation and Communication
Once the new version is live, comprehensive documentation must be published. This includes:- A changelog highlighting what’s new or changed
- Migration guides for clients
- Clear communication about the deprecation timeline for the previous version
- Grace Period and Sunset Policy
During the grace period, both versions remain operational. This gives clients time to test, adapt, and migrate. It’s crucial to communicate the end-of-life date for the old version well in advance. - Sunsetting the Old Version
Once the grace period ends, the previous version is officially retired. The development team removes it from production, ensuring that only the latest version remains active.
Versioning Isn’t Just Technical—It’s Organizational
The debate around API versioning, endpoint versioning, and backward compatibility often sounds like a technical challenge—but in reality, it’s more of an organizational issue.
When teams communicate effectively, many of the problems that lead to complex versioning schemes can be avoided altogether. If backend and frontend teams (or producers and consumers) are aligned early and often, there’s usually no need to version individual endpoints. Most changes can be coordinated and rolled out together.
The problem arises when communication breaks down. That’s when teams start introducing endpoint-level versioning as a safety net. But here’s the catch: endpoint versioning comes with a maintenance cost. Over time, it becomes unclear which clients are using which versions. You end up digging through logs to figure out if an old endpoint is still in use—and whether it’s safe to remove.
A cleaner, more sustainable approach is to version the entire application and API together:
- Release a new version of the application with updated APIs.
- Migrate clients to the new version.
- Once migration is complete, decommission the old version.
This avoids the accumulation of dead code and keeps your codebase lean and maintainable. It also simplifies testing and deployment, especially when combined with automated contract testing and CI/CD pipelines that validate compatibility before release.
“Always be a first-rate version, instead of a second-rate version.”
By treating versioning as a product lifecycle decision—not just a technical one—you create a healthier development culture and a more predictable experience for your consumers.