When to Deprecate APIs: The Complete Guide for Java Architects and Quarkus Developers
Learn the strategy, communication, and hands-on Quarkus patterns every Java developer needs to manage API versioning without breaking production systems.
APIs are forever. Or at least they feel that way. Once an endpoint is published, it will be called by systems you’ve never met, in environments you’ll never control, for longer than you ever intended. This is why API deprecation is one of the hardest decisions an architect faces.
If you change a database column, you only affect your own team. If you change a REST endpoint, you affect production systems run by other people like partners, customers, unknown consumers who found your API years ago and quietly built on it. Breaking them may mean contractual penalties, legal disputes, or at the very least, angry emails at 3 a.m.
Deprecation is not just a technical process. It is a political and strategic act. You must weigh security against stability, innovation against trust, cost against risk. And once you’ve decided, you must communicate with absolute clarity.
This article walks through a framework for deciding when to deprecate, how to plan and communicate the change, and which Java + Quarkus implementation patterns help you manage the lifecycle.
Why API Deprecation Matters in the Enterprise
Modern enterprises rely on APIs as their nervous system. Payments, logistics, identity, compliance, partner integrations—everything flows through published endpoints.
You don’t control the client code. Your consumers deploy on their own schedules.
Breaking changes hit live systems. You can’t test their production stack.
Legal/financial risks are real. SLAs and contracts bind you to stability.
The “unknown consumer” problem. Public endpoints get used in ways you never anticipated.
This is why deprecation must be deliberate.
When to Deprecate Remote APIs
Not every inconvenience justifies deprecation. Reserve it for cases where continued support is more dangerous than change.
Security vulnerabilities you cannot patch in place.
Architectural shifts like moving from monoliths to microservices, or synchronous to event-driven.
Data model changes where maintaining backward compatibility adds untenable complexity.
High maintenance cost with low actual usage.
Compliance mandates such as GDPR’s right-to-be-forgotten or regional data residency.
Strategic Deprecation Patterns
Every architect should keep these patterns in mind:
Version Everything
Don’t publish without a versioning plan. Path (/v1/
), header (X-API-Version
), or media type (application/vnd.company.v2+json
).Grace Periods
Communicate in months, not weeks. Typical enterprise windows:Security fix only: 3–6 months
Business-critical APIs: 12–24 months
Dual Running
Keep old and new APIs live in parallel. Build transformation layers when possible.Progressive Enforcement
Start with warnings.
Add headers (
Deprecation
,Sunset
).Introduce rate limiting.
Eventually return
410 Gone
.
Measure and Decide with Data
Instrument every deprecated endpoint. Actual usage should dictate timelines.
Communication Is Everything
A smooth migration depends less on code and more on communication.
Developer portals: Flag deprecated endpoints clearly, link migration guides.
Email campaigns: Reach out via registered API keys.
Webhook notifications: Alert developers automatically when they hit a deprecated endpoint.
Status pages & public roadmaps: Build transparency and trust.
Think like a product manager, not just an engineer.
Hands-On: Quarkus Implementation Patterns
Let’s build a small Quarkus project to demonstrate deprecation in practice.
Project Setup
quarkus create app org.acme:rest-deprecation-practice:1.0.0 \
--no-code -x rest-jackson,io.quarkus:quarkus-logging-json,hibernate-validator,quarkus-micrometer,quarkus-micrometer-registry-prometheus
cd rest-deprecation-practice
This command pulls in a set of Quarkus extensions that directly support deprecation workflows:
rest-jackson
Adds JSON serialization and deserialization via Jackson. This makes it easy to define DTOs per version and return structured responses.logging-json
Configures structured JSON logs out of the box. Useful for tracking deprecated endpoint calls and extracting client identifiers in log pipelines.hibernate-validator
Provides Jakarta Bean Validation. This lets you enforce schema rules on incoming requests and automatically reject bad input with400 Bad Request
.quarkus-micrometer
Integrates Micrometer metrics. Together with the Prometheus registry, it exposes counters that track usage of deprecated endpoints, helping you decide when to finally remove them.quarkus-micrometer-registry-prometheus
Exposes Micrometer metrics in Prometheus format under/q/metrics
. This allows you to track deprecation adoption in Grafana dashboards or any Prometheus-compatible system.
Grab the full project from my Github if you like!
DTOs and Resources with Validation
When running APIs in parallel versions, each version should have its own DTOs (Data Transfer Objects). This ensures that schema evolution is explicit, avoids regressions, and makes deprecation boundaries clear. Hibernate Validator lets you enforce input quality at the API boundary, failing early if clients send bad data.
V1 Order Request and Response DTOs
Represents a simplified order request in version 1. It contains only product and quantity fields, with minimal validation. Also another representation of the API’s response for v1, echoing back basic order details.
package com.example.api.v1.dto;
import jakarta.validation.constraints.NotBlank;
public class OrderRequestV1 {
@NotBlank
public String product;
public int quantity;
}
package com.example.api.v1.dto;
public class OrderResponseV1 {
public String orderId;
public String product;
public int quantity;
}
V2 Order Request and Response DTOs
Represents an evolved order request schema in version 2. Adds customer identification and stricter validation with the Response DTO implementing the richer response schema in v2, now including customer information and order status.
package com.example.api.v2.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Positive;
public class OrderRequestV2 {
@NotBlank
public String product;
@Positive
public int quantity;
@NotBlank
public String customerId;
}
package com.example.api.v2.dto;
public class OrderResponseV2 {
public String orderId;
public String product;
public int quantity;
public String customerId;
public String status;
}
Deprecated v1 Resource
A Quarkus REST resource for handling order creation in v1. It marks the endpoint as deprecated, logs usage, and records metrics.
package com.example.api.v1;
import org.jboss.logging.Logger;
import com.example.api.v1.dto.OrderRequestV1;
import com.example.api.v1.dto.OrderResponseV1;
import io.micrometer.core.annotation.Counted;
import jakarta.validation.Valid;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
@Path(”/api/v1/orders”)
public class OrderResourceV1 {
private static final Logger LOG = Logger.getLogger(OrderResourceV1.class);
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Counted(value = “deprecated_orders_v1_requests_total”, description = “Number of calls to deprecated/api/v1/orders endpoint”)
public Response createOrder(@Valid OrderRequestV1 request,
@Context HttpHeaders headers) {
String clientId = headers.getHeaderString(”X-Client-Id”);
LOG.warnf(”Deprecated v1 order endpoint called by client: %s”, clientId);
OrderResponseV1 response = new OrderResponseV1();
response.orderId = “ORD-123”;
response.product = request.product;
response.quantity = request.quantity;
return Response.ok(response)
.header(”Deprecation”, “true”)
.header(”Sunset”, “Wed, 30 Sep 2026 00:00:00 GMT”)
.build();
}
}
@Counted
increments a counter every time the endpoint is called.Deprecation
andSunset
headers tell clients the endpoint is being retired.LOG.warnf
records client identifiers for traceability.@Valid
ensures invalid requests are rejected automatically.
Replacement API (v2)
A Quarkus REST resource for v2. It uses stricter validation, generates real UUIDs, and returns the enriched response DTO.
package com.example.api.v2;
import java.util.UUID;
import com.example.api.v2.dto.OrderRequestV2;
import com.example.api.v2.dto.OrderResponseV2;
import jakarta.validation.Valid;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
@Path(”/api/v2/orders”)
public class OrderResourceV2 {
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response createOrder(@Valid OrderRequestV2 request) {
OrderResponseV2 response = new OrderResponseV2();
response.orderId = UUID.randomUUID().toString();
response.product = request.product;
response.quantity = request.quantity;
response.customerId = request.customerId;
response.status = “NEW”;
return Response.ok(response).build();
}
}
Generates unique
orderId
withUUID
.Enforces stricter schema with
@Valid
.Adds
status
field to represent order lifecycle.
application.properties
quarkus.log.console.json=true
quarkus.log.level=INFO
Testing API Deprecation with Quarkus
Quarkus integrates tightly with JUnit 5. With @QuarkusTest
, you can spin up your API in test mode and verify deprecation behavior.
package com.example;
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
@QuarkusTest
public class ApiDeprecationTest {
@Test
void v1EndpointShouldReturnDeprecationHeaders() {
given()
.header(”Content-Type”, “application/json”)
.header(”X-Client-Id”, “test-client”)
.body(”{\”product\”:\”Book\”,\”quantity\”:1}”)
.when()
.post(”/api/v1/orders”)
.then()
.statusCode(200)
.header(”Deprecation”, equalTo(”true”))
.header(”Sunset”, containsString(”2026”))
.body(”product”, equalTo(”Book”))
.body(”quantity”, equalTo(1));
}
@Test
void v1EndpointShouldRejectInvalidRequests() {
given()
.header(”Content-Type”, “application/json”)
.header(”X-Client-Id”, “bad-client”)
.body(”{\”product\”:\”\”,\”quantity\”:0}”) // invalid
.when()
.post(”/api/v1/orders”)
.then()
.statusCode(400);
}
@Test
void v2EndpointShouldAcceptValidRequests() {
given()
.header(”Content-Type”, “application/json”)
.body(”{\”product\”:\”Pen\”,\”quantity\”:3,\”customerId\”:\”cust-42\”}”)
.when()
.post(”/api/v2/orders”)
.then()
.statusCode(200)
.body(”status”, equalTo(”NEW”))
.body(”customerId”, equalTo(”cust-42”))
.body(”quantity”, equalTo(3));
}
@Test
void v2EndpointShouldRejectInvalidRequests() {
given()
.header(”Content-Type”, “application/json”)
.body(”{\”product\”:\”\”,\”quantity\”:-1,\”customerId\”:\”\”}”) // invalid
.when()
.post(”/api/v2/orders”)
.then()
.statusCode(400);
}
@Test
void metricsShouldCountDeprecatedUsage() {
// Call v1 endpoint
given()
.header(”Content-Type”, “application/json”)
.header(”X-Client-Id”, “metrics-client”)
.body(”{\”product\”:\”Book\”,\”quantity\”:1}”)
.when()
.post(”/api/v1/orders”)
.then()
.statusCode(200);
// Verify metric is incremented
given()
.when()
.get(”/q/metrics”)
.then()
.statusCode(200)
.body(containsString(”deprecated_orders_v1_requests_total”));
}
}
RestAssured (bundled with Quarkus test extensions) makes endpoint testing concise.
The first test checks for
Deprecation
andSunset
headers in v1 responses.The second and fourth tests confirm that invalid DTOs trigger
400 Bad Request
automatically via Hibernate Validator.The metrics test ensures
deprecated_orders_v1_requests_total
is exposed and increments after usage.Tests provide confidence that both deprecation signaling and validation are working correctly.
Also make sure to add the following dependency to your applications pom.xml
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
Run and Verify
quarkus dev
Test deprecated endpoint:
curl -X POST -H “Content-Type: application/json” -H “X-Client-Id: test-client” -d “{\”product\”:\”Book\”,\”quantity\”:1}” http://localhost:8080/api/v1/orders
Expected response:
> POST /api/v1/orders HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/8.7.1
> Accept: */*
> Content-Type: application/json
> X-Client-Id: test-client
> Content-Length: 31
>
* upload completely sent off: 31 bytes
< HTTP/1.1 200 OK
< Content-Type: application/json;charset=UTF-8
< content-length: 51
< Deprecation: true
< Sunset: Wed, 30 Sep 2026 00:00:00 GMT
<
* Connection #0 to host localhost left intact
{”orderId”:”ORD-123”,”product”:”Book”,”quantity”:1}
Logs show:
{
“timestamp”: “2025-09-30T12:51:25.09731+02:00”,
“sequence”: 2446,
“loggerClassName”: “org.jboss.logging.Logger”,
“loggerName”: “com.example.api.v1.OrderResourceV1”,
“level”: “WARN”,
“message”: “Deprecated v1 order endpoint called by client: test-client”,
“threadName”: “executor-thread-1”,
“threadId”: 165,
“mdc”: {},
“ndc”: “”,
“hostName”: “mymachine”,
“processName”: “/Library/Java/JavaVirtualMachines/temurin-21.jdk/Contents/Home/bin/java”,
“processId”: 11157
}
Check metrics:
curl http://localhost:8080/q/metrics | grep deprecated_orders_v1_requests_total
Expected output:
deprecated_orders_v1_requests_total{class=”com.example.api.v1.OrderResourceV1”,exception=”none”,method=”createOrder”,result=”success”} 1.0
Advanced Patterns
Gradual degradation: Add rate limiting via Quarkus
smallrye-mutiny
or Bucket4j.Feature flags: Use
quarkus-config-yaml
or external toggles to manage endpoint lifecycle.Canary deployments: Route a fraction of clients to
/v2/
.Metrics: Expose Prometheus counters for deprecated usage (
@Counted
fromquarkus-micrometer
).
Real-World Lessons from API Deprecation
Large platforms have navigated API deprecation under very public pressure. Their choices highlight both good practices and painful missteps.
Twitter’s transition from API v1.1 to v2 is a cautionary tale. In September 2023, the company formally deprecated v1.1 endpoints, forcing developers onto the newer v2 model. Many endpoints were removed outright, with no one-to-one replacements, leaving developers scrambling to re-engineer integrations. Migration guides existed, such as Twitter’s own mapping for tweet lookups, but communication was uneven. Some developers only discovered the change when production calls started failing, as noted in support advisories and community complaints (e.g. GitHub issue). The lesson is clear: removing endpoints without direct replacements or long lead times creates frustration and operational risk. Deprecation must be paired with transparency, multi-channel communication, and sufficient grace periods.
Stripe offers a different model. One many architects hold up as best practice. The company’s policy is explicit: APIs are not deprecated unless absolutely necessary. When they introduced the new Payment Intents API, they continued supporting the legacy charges API for non-EU use cases, while steering new customers to the updated model. Their public upgrade guide (Stripe docs) shows how they provide clear changelogs and opt-in versioning. Internally, Stripe maintains only the latest implementation, translating requests and responses for older versions through adapter layers rather than duplicating business logic (Lethain’s write-up). The lesson here is twofold: avoid deprecation unless there is no alternative, and use translation layers to decouple client versions from backend evolution. This approach reduces breakage but requires discipline to prevent technical debt from piling up in the adapters.
Beyond high-profile platforms, research also shows how uneven deprecation practices are in the wild. An empirical study of thousands of APIs found that many services annotate operations as deprecated but fail to provide removal dates or migration paths. Some endpoints remain in limbo, marked “deprecated” for years without ever being removed (arXiv study). This creates confusion for consumers, who cannot tell whether a deprecated feature is safe to rely on or moments away from removal. The key lesson is that deprecation must be a lifecycle with clear stages: marked as deprecated, supported for a defined period, then removed. Anything else erodes trust.
Together, these examples underline a simple rule: deprecation is as much about trust as it is about technology. Twitter showed the cost of moving too fast with too little communication. Stripe demonstrated the value of minimizing deprecation and providing adapter layers. Academic research confirmed that many APIs fail because they treat deprecation as a symbolic label rather than a managed process. For Java architects designing enterprise APIs, the takeaway is to plan for deprecation as deliberately as you plan for availability and security.
The Migration Path
A smooth migration depends on careful staging. The safest approach is to run the old and new API versions in parallel, giving consumers time to switch without breaking production workloads. Where possible, build request and response translation layers so older clients can continue to function while they gradually move over. Updated client SDKs should be released early, lowering the effort required for consumers to adopt the new endpoints. Finally, monitor actual adoption closely and proactively reach out to laggards, nudging them before deadlines arrive.
Recommendations for Architects
Never publish an API without a versioning strategy.
Always instrument usage before planning deprecation.
Treat communication as part of the release.
Plan deprecation as a process, not a one-time event.
Deprecation is not failure. It’s governance. Handle it well, and you’ll be trusted to innovate again.