API Versioning Strategies for Long-Lived Systems cover image

API Versioning Strategies for Long-Lived Systems

By Julian Krause · Saturday, March 8, 2025 · ~3 min read

Every API that survives long enough will need versioning. The question is not whether to version, but how to do it in a way that minimizes pain for both your team and your consumers. After maintaining public APIs for over six years across multiple products, I want to share what has worked and what has not.

URL Segment Versioning — Simple and Honest

URL segment versioning (/api/v1/orders) is the most common approach, and for good reason. It is explicit, easy to route, and impossible to miss. In .NET, the Asp.Versioning.Http package makes this straightforward.

builder.Services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ReportApiVersions = true;
    options.ApiVersionReader = new UrlSegmentApiVersionReader();
})
.AddApiExplorer(options =>
{
    options.GroupNameFormat = "'v'VVV";
    options.SubstituteApiVersionInUrl = true;
});

The downside is URL pollution — every route now carries version baggage, and clients need to update URLs when migrating. But in practice, this explicitness is a feature, not a bug.

Header Versioning — Clean URLs, Hidden Complexity

Header-based versioning (Api-Version: 2) keeps URLs clean but hides the version from casual inspection. Every debugging session, every curl command, every log entry becomes slightly harder because the version is not visible in the URL.

options.ApiVersionReader = new HeaderApiVersionReader("X-Api-Version");

// Client must remember to set the header
httpClient.DefaultRequestHeaders.Add("X-Api-Version", "2");

I have seen this work well for internal APIs where all consumers are controlled by the same organization. For public APIs, I strongly prefer URL versioning.

Additive Changes Avoid Versioning Entirely

The best version change is no version change. Many "breaking changes" can be restructured as additive changes. Adding a new field to a response is not breaking. Adding a new optional query parameter is not breaking. Adding a new endpoint is not breaking.

// V1 response
public class OrderResponse
{
    public Guid Id { get; init; }
    public string Status { get; init; }
    public decimal Total { get; init; }
}

// Still V1 — adding fields is non-breaking
public class OrderResponse
{
    public Guid Id { get; init; }
    public string Status { get; init; }
    public decimal Total { get; init; }
    public string? CurrencyCode { get; init; } // New, nullable, non-breaking
    public DateTimeOffset? EstimatedDelivery { get; init; } // New, nullable, non-breaking
}

Sunset Policies Save You

Document and enforce sunset policies from day one. When you release v2, announce that v1 will be supported for exactly 12 months. Set Sunset and Deprecation headers on old versions. Monitor usage and reach out to consumers who have not migrated.

app.MapGet("/api/v1/orders", (HttpContext context) =>
{
    context.Response.Headers.Append("Sunset", "Sat, 01 Mar 2026 00:00:00 GMT");
    context.Response.Headers.Append("Deprecation", "true");
    context.Response.Headers.Append("Link",
        "</api/v2/orders>; rel=\"successor-version\"");

    // ... handle request
});

Without sunset policies, you end up maintaining every version indefinitely. I have worked on systems with five active API versions and the maintenance burden is brutal. Set boundaries early, communicate them clearly, and enforce them on schedule.


Comments (2)

Elena Tuesday, March 3, 2026 5:08 PM

I tried this approach and it works perfectly!

Greta Tuesday, March 3, 2026 8:08 PM

Thanks for your comment — glad it helped!

Felix Wednesday, March 4, 2026 5:08 PM

Well written and easy to follow. Keep it up!

Leave a Comment