This project has been relocated to OG4Dev Spring Response
New Coordinates:
<dependency>
<groupId>io.github.og4dev</groupId>
<artifactId>og4dev-spring-response</artifactId>
<version>1.0.0</version>
</dependency>
io.github.og4dev:og4dev-spring-response:1.0.0 for future updates and support.
A lightweight, type-safe API Response wrapper for Spring Boot applications. Standardize your REST API responses with consistent structure, automatic timestamps, distributed tracing support, and clean factory methods. Features zero-configuration Spring Boot auto-configuration and production-ready exception handling with comprehensive RFC 9457 ProblemDetail support covering 10 common error scenarios. No external dependencies required - uses pure Java with a custom builder pattern.
- π NEW PROJECT - OG4Dev Spring Response - Continue development here!
- π¦ Maven Central Repository (Deprecated)
- π JavaDoc Documentation
- π Report Issues (Use new project)
- π‘ Feature Requests (Use new project)
- π€ Contributing Guide (Use new project)
- Quick Links
- π¨ IMPORTANT: Migration to OG4Dev
- Key Highlights
- Features
- Requirements
- What Makes This Different?
- Installation
- Project Structure
- Quick Start
- Auto-Configuration
- Built-in Exception Handling
- Usage
- Real-World Examples
- API Reference
- Response Structure
- Best Practices
- Testing
- Architecture & Design
- OpenAPI/Swagger Integration
- Compatibility Matrix
- Troubleshooting
- FAQ
- Performance & Best Practices
- Migration Guide
- Security Considerations
- Contributing
- License
- Contact
- Acknowledgments
- Version History
io.github.pasinduog:api-response β io.github.og4dev:og4dev-spring-response
We have rebranded this library and moved it to the OG4Dev organization for better community support and long-term maintenance.
Maven:
<!-- REMOVE THIS (Deprecated) -->
<dependency>
<groupId>io.github.pasinduog</groupId>
<artifactId>api-response</artifactId>
<version>3.0.1</version>
</dependency>
<!-- ADD THIS (New) -->
<dependency>
<groupId>io.github.og4dev</groupId>
<artifactId>og4dev-spring-response</artifactId>
<version>1.0.0</version>
</dependency>Gradle:
// REMOVE THIS (Deprecated)
implementation 'io.github.pasinduog:api-response:3.0.1'
// ADD THIS (New)
implementation 'io.github.og4dev:og4dev-spring-response:1.0.0'The API is 100% compatible. You don't need to change any code - just update the dependency coordinates.
// This works exactly the same in both versions
return ApiResponse.success("User found", user);
return ApiResponse.created("User created", newUser);- β Continued Support - Active development and bug fixes
- β New Features - Latest enhancements and improvements
- β Better Organization - Professional organization structure
- β Community Support - Enhanced collaboration and contribution
- β Long-term Maintenance - Guaranteed ongoing support
β οΈ No New Features - Version 3.0.1 is the final releaseβ οΈ No Bug Fixes - Critical issues won't be addressedβ οΈ No Support - Questions and issues should go to new project- β Still Works - Existing functionality continues to work
Visit the new project for:
- π Updated documentation
- π Issue reporting
- π¬ Community discussions
- π Latest features
New Repository: https://github.com/og4dev/og4dev-spring-response
- π Truly Zero Configuration - Spring Boot 3.x/4.x auto-configuration with META-INF imports
- π― Production-Ready - Built-in RFC 9457 ProblemDetail with 10 comprehensive exception handlers
- π‘οΈ Complete Error Coverage - Handles validation, JSON parsing, 404s, method mismatches, media types, and more (Enhanced in v3.0.0)
- π Trace IDs in Errors Only - Error responses include traceId for debugging (Changed in v3.0.0)
- π Type-Safe & Immutable - Thread-safe design with generic type support
- π¦ Ultra-Lightweight - Only ~10KB JAR size with provided dependencies
- π Microservices-Ready - Built-in trace IDs for distributed tracing
- β Battle-Tested - Used in production Spring Boot applications
- π Clean Javadoc - Zero warnings with explicit constructor documentation (New in v3.0.0)
- π« Zero External Dependencies - Pure Java, no Lombok required (Changed in v3.0.0)
- π― Consistent Structure - All responses follow the same format:
status,message,content,timestamp - π Type-Safe - Full generic type support with compile-time type checking
- π Distributed Tracing - Trace IDs in error responses with MDC integration for request tracking (Enhanced in v3.0.0)
- β° Auto Timestamps - Automatic RFC 3339 UTC formatted timestamps on every response
- π Factory Methods - Clean static methods:
success(),created(),status() - π Zero Config - Spring Boot Auto-Configuration for instant setup (Enhanced in v1.3.0)
- πͺΆ Lightweight - Only ~10KB JAR with single provided dependency (Spring Web)
- π¦ Immutable - Thread-safe with final fields
- π Spring Native - Built on
ResponseEntityandHttpStatus - π RFC 9457 Compliance - Standard ProblemDetail format (supersedes RFC 7807) (Updated in v3.0.0)
- π Complete JavaDoc - Every class fully documented with explicit constructor documentation (New in v3.0.0)
- π‘οΈ Comprehensive Exception Handling - 10 built-in handlers covering all common scenarios (Enhanced in v3.0.0)
- β
Validation errors (
@Validannotations) - β Type mismatches (wrong parameter types)
- β Malformed JSON (invalid request bodies)
- β
Missing parameters (required
@RequestParam) - β 404 Not Found (missing endpoints/resources)
- β 405 Method Not Allowed (wrong HTTP method)
- β 415 Unsupported Media Type (invalid Content-Type)
- β Null pointer exceptions
- β
Custom business exceptions (
ApiException) - β General unexpected errors
- β
Validation errors (
- π Custom Business Exceptions - Abstract
ApiExceptionclass for domain-specific errors (New in v1.2.0) - β
Validation Support - Automatic
@Validannotation error handling
- Java 17 or higher
- Spring Boot 3.2.0 or higher (tested up to 4.0.2)
- No additional dependencies required (pure Java implementation)
Unlike other response wrapper libraries, this one offers:
- β Native Spring Boot 3.x/4.x Auto-Configuration - No manual setup required
- β RFC 9457 ProblemDetail Support - Industry-standard error responses (latest RFC)
- β Zero External Dependencies - Pure Java implementation, won't conflict with your application
- β Extensible Exception Handling - Create custom business exceptions easily
- β Trace ID Support - Built-in distributed tracing capabilities
- β Comprehensive JavaDoc - Every class fully documented with explicit constructor documentation and zero warnings
- β Production-Grade Quality - Clean builds, proper documentation, and battle-tested code
<dependency>
<groupId>io.github.pasinduog</groupId>
<artifactId>api-response</artifactId>
<version>3.0.0</version>
</dependency>implementation 'io.github.pasinduog:api-response:3.0.0'implementation("io.github.pasinduog:api-response:3.0.0")If you need the previous stable version with Lombok:
<dependency>
<groupId>io.github.pasinduog</groupId>
<artifactId>api-response</artifactId>
<version>2.0.0</version>
</dependency>Note: Version 2.0.0 includes 6 additional exception handlers and enhanced features compared to v1.3.0. See Version History for details.
The library is organized into four main packages:
io.github.pasinduog
βββ config/
β βββ ApiResponseAutoConfiguration.java # Spring Boot auto-configuration
βββ dto/
β βββ ApiResponse.java # Generic response wrapper
βββ exception/
β βββ ApiException.java # Abstract base for custom exceptions
β βββ GlobalExceptionHandler.java # RFC 9457 exception handler
βββ filter/
βββ TraceIdFilter.java # Request trace ID generation
| Package | Description |
|---|---|
config |
Spring Boot auto-configuration classes for zero-config setup |
dto |
Data Transfer Objects - main ApiResponse<T> wrapper class |
exception |
Exception handling infrastructure with ProblemDetail support |
filter |
Servlet filters for trace ID generation and MDC management |
- ApiResponse - Type-safe response wrapper with factory methods
- ApiResponseAutoConfiguration - Automatic Spring Boot integration
- GlobalExceptionHandler - Centralized exception handling with RFC 9457
- ApiException - Base class for domain-specific exceptions
- TraceIdFilter - Automatic trace ID generation for distributed tracing
import io.github.pasinduog.dto.ApiResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/{id}")
public ResponseEntity<ApiResponse<User>> getUser(@PathVariable Long id) {
User user = userService.findById(id);
return ApiResponse.success("User retrieved successfully", user);
}
}Response:
{
"status": 200,
"message": "User retrieved successfully",
"content": {
"id": 1,
"name": "John Doe",
"email": "john@example.com"
},
"timestamp": "2026-02-06T10:30:45.123Z"
}The library now features Spring Boot Auto-Configuration for truly zero-config setup! Simply add the dependency and everything works automatically.
When you include this library in your Spring Boot application, the following components are automatically registered:
β
GlobalExceptionHandler - Automatic exception handling with RFC 7807 ProblemDetail format
β
Component Scanning - All library components are automatically discovered
β
Bean Registration - No manual @ComponentScan or @Import required
The library includes META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports which Spring Boot 3.x automatically detects and loads the ApiResponseAutoConfiguration class.
No configuration needed! Just add the dependency:
<dependency>
<groupId>io.github.pasinduog</groupId>
<artifactId>api-response</artifactId>
<version>2.0.0</version>
</dependency>If you need to customize or disable the auto-configuration:
@SpringBootApplication(exclude = ApiResponseAutoConfiguration.class)
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}Or in application.properties:
spring.autoconfigure.exclude=io.github.pasinduog.config.ApiResponseAutoConfigurationThe library includes a production-ready GlobalExceptionHandler that automatically handles common exceptions using Spring Boot's ProblemDetail (RFC 9457) standard.
β
General Exception Handler - Catches all unhandled exceptions (HTTP 500)
β
Validation Error Handler - Automatically processes @Valid annotation failures (HTTP 400)
β
Type Mismatch Handler - Handles method argument type conversion errors (HTTP 400) (New in v1.3.0)
β
Malformed JSON Handler - Handles invalid JSON request bodies (HTTP 400) (New in v2.0.0)
β
Missing Parameter Handler - Detects missing required request parameters (HTTP 400) (New in v2.0.0)
β
404 Not Found Handler - Handles missing endpoints and resources (HTTP 404) (New in v2.0.0)
β
Method Not Allowed Handler - Handles unsupported HTTP methods (HTTP 405) (New in v2.0.0)
β
Unsupported Media Type Handler - Handles invalid Content-Type headers (HTTP 415) (New in v2.0.0)
β
Null Pointer Handler - Specific handling for NullPointerException (HTTP 500)
β
Custom ApiException Handler - Handles custom business exceptions extending ApiException (New in v1.2.0)
β
Automatic Logging - SLF4J integration for all errors with consistent trace IDs (Enhanced in v2.0.0)
β
Trace ID Consistency - Logs and responses always have matching trace IDs (New in v2.0.0)
β
Timestamp Support - All error responses include RFC 3339 timestamps
β
RFC 9457 Compliance - Standard ProblemDetail format (supersedes RFC 7807) (New in v2.0.0)
All exception handlers now ensure consistent trace IDs between logs and error responses:
- With TraceIdFilter: Uses the trace ID from SLF4J MDC
- Without Filter: Generates a UUID and stores it in MDC for consistent logging
- Guaranteed: Logs and responses always have matching trace IDs
Example Log Output:
2026-02-07 10:30:45.123 [550e8400-e29b-41d4-a716-446655440000] ERROR GlobalExceptionHandler - Error in SQLExceptionTranslator:112
Matching Error Response:
{
"type": "about:blank",
"title": "Internal Server Error",
"status": 500,
"detail": "Internal Server Error. Please contact technical support",
"traceId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-02-07T10:30:45.123Z"
}β
No more "N/A" trace IDs - Every error has a real, correlatable trace ID
β
Easy debugging - Copy trace ID from response and find all related logs
β
Thread-safe - Proper MDC management ensures no cross-thread contamination
Instead of throwing generic exceptions, you can now extend the abstract ApiException class to create domain-specific exceptions with automatic exception handling:
public class ResourceNotFoundException extends ApiException {
public ResourceNotFoundException(String resource, Long id) {
super(String.format("%s not found with ID: %d", resource, id), HttpStatus.NOT_FOUND);
}
}
public class UnauthorizedAccessException extends ApiException {
public UnauthorizedAccessException(String message) {
super(message, HttpStatus.UNAUTHORIZED);
}
}
public class BusinessRuleViolationException extends ApiException {
public BusinessRuleViolationException(String message) {
super(message, HttpStatus.CONFLICT);
}
}Usage in Controllers:
@GetMapping("/{id}")
public ResponseEntity<ApiResponse<User>> getUser(@PathVariable Long id) {
User user = userService.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("User", id));
return ApiResponse.success("User retrieved successfully", user);
}Automatic Error Response (RFC 9457):
{
"type": "about:blank",
"title": "Not Found",
"status": 404,
"detail": "User not found with ID: 123",
"traceId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-02-02T10:30:45.123Z"
}The GlobalExceptionHandler automatically:
- Extracts the HTTP status from your custom exception
- Formats it as a ProblemDetail response
- Logs the error with appropriate severity
- Includes timestamps for traceability
Add validation to your DTOs:
public class UserDto {
@NotBlank(message = "Name is required")
private String name;
@Email(message = "Email must be valid")
@NotBlank(message = "Email is required")
private String email;
@Min(value = 18, message = "Age must be at least 18")
private Integer age;
}Use @Valid in your controller:
@PostMapping
public ResponseEntity<ApiResponse<User>> createUser(@Valid @RequestBody UserDto dto) {
User newUser = userService.create(dto);
return ApiResponse.created("User created successfully", newUser);
}Automatic Error Response:
{
"type": "about:blank",
"title": "Bad Request",
"status": 400,
"detail": "Validation Failed",
"errors": {
"email": "Email must be valid",
"name": "Name is required",
"age": "Age must be at least 18"
},
"traceId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-02-02T10:30:45.123Z"
}When a client sends invalid JSON (missing quotes, commas, etc.):
# Request with malformed JSON
POST /api/users
Content-Type: application/json
{
"name": "John Doe"
"email": "invalid" # Missing comma
}Automatic Error Response:
{
"type": "about:blank",
"title": "Bad Request",
"status": 400,
"detail": "Malformed JSON request. Please check your request body format.",
"traceId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-02-02T10:30:45.123Z"
}When a required @RequestParam is missing:
@GetMapping("/search")
public ResponseEntity<ApiResponse<List<User>>> search(
@RequestParam(required = true) String query) {
// ...
}Automatic Error Response:
{
"type": "about:blank",
"title": "Bad Request",
"status": 400,
"detail": "Required request parameter 'query' (type: String) is missing.",
"traceId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-02-02T10:30:45.123Z"
}When accessing a non-existent endpoint or resource:
GET /api/nonexistentAutomatic Error Response:
{
"type": "about:blank",
"title": "Not Found",
"status": 404,
"detail": "The requested resource '/api/nonexistent' was not found.",
"traceId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-02-02T10:30:45.123Z"
}When using an unsupported HTTP method:
# POST to an endpoint that only supports GET
POST /api/users/123Automatic Error Response:
{
"type": "about:blank",
"title": "Method Not Allowed",
"status": 405,
"detail": "Method 'POST' is not supported for this endpoint. Supported methods are: [GET, PUT, DELETE]",
"traceId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-02-02T10:30:45.123Z"
}When sending an unsupported Content-Type:
POST /api/users
Content-Type: application/xml
<user><name>John</name></user>Automatic Error Response:
{
"type": "about:blank",
"title": "Unsupported Media Type",
"status": 415,
"detail": "Content type 'application/xml' is not supported. Supported content types: [application/json]",
"traceId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-02-02T10:30:45.123Z"
}All exceptions are automatically logged with appropriate severity levels:
- ERROR level - General exceptions and null pointer exceptions
- WARN level - Validation errors, type mismatches, malformed JSON, missing parameters, 404 errors, method not allowed, unsupported media types, and business logic exceptions
2026-02-02 10:30:45.123 WARN i.g.p.e.GlobalExceptionHandler - Validation error: {email=Email must be valid, name=Name is required}
2026-02-02 10:30:45.456 ERROR i.g.p.e.GlobalExceptionHandler - An unexpected error occurred:
java.lang.RuntimeException: Database connection failed
at com.example.service.UserService.findById(UserService.java:42)
2026-02-02 10:31:12.789 WARN i.g.p.e.GlobalExceptionHandler - Malformed JSON request: JSON parse error: Unexpected character...
2026-02-02 10:31:45.234 WARN i.g.p.e.GlobalExceptionHandler - Missing parameter: Required request parameter 'query' (type: String) is missing.
2026-02-02 10:32:15.567 WARN i.g.p.e.GlobalExceptionHandler - 404 Not Found: The requested resource '/api/invalid' was not found.
2026-02-02 10:32:45.890 WARN i.g.p.e.GlobalExceptionHandler - Method not allowed: Method 'POST' is not supported for this endpoint.
2026-02-02 10:33:15.123 WARN i.g.p.e.GlobalExceptionHandler - Unsupported media type: Content type 'application/xml' is not supported.
## π Distributed Tracing (Enhanced in v2.0.0)
The library provides **production-ready distributed tracing** with automatic trace ID generation and MDC (Mapped Diagnostic Context) integration for seamless log correlation across your microservices architecture.
### Key Features
β
**Automatic Trace ID Generation** - Every response includes a unique UUID for request tracking
β
**MDC Integration** - Trace IDs automatically available in all log statements via SLF4J MDC
β
**TraceIdFilter** - Optional servlet filter for consistent trace ID management *(New in v2.0.0)*
β
**Flexible Priority** - Supports explicit trace IDs, MDC context, or auto-generation
β
**Thread-Safe** - Proper MDC cleanup prevents memory leaks
β
**Zero Configuration** - Works out of the box with sensible defaults
β
**UUID Format** - Standard 128-bit globally unique identifiers
### Industry Standards Compatibility
The trace ID implementation uses **UUID format** (128-bit), which is:
- β
Widely supported across distributed systems
- β
Compatible with most logging and APM tools
- β
Globally unique without coordination
**For enhanced interoperability**, consider implementing header propagation to support:
- `X-Trace-Id` / `X-Request-ID` (Common custom headers)
- `X-B3-TraceId` (Zipkin/B3 format)
- `traceparent` (W3C Trace Context standard)
See the [Enhanced TraceIdFilter](#enhanced-traceidfilter-with-header-propagation) section below for implementation details.
### How It Works
#### Default Behavior (Without Filter)
By default, `ApiResponse` automatically generates a trace ID for each response:
```java
@GetMapping("/{id}")
public ResponseEntity<ApiResponse<User>> getUser(@PathVariable Long id) {
User user = userService.findById(id);
return ApiResponse.success("User found", user);
}
Response:
{
"status": 200,
"message": "User found",
"content": { "id": 1, "name": "John" },
"timestamp": "2026-02-06T10:30:45.123Z"
}Note: Success responses (ApiResponse) do NOT include traceId. Trace IDs are only included in error responses (ProblemDetail format) for debugging purposes.
#### Enhanced Tracing with TraceIdFilter *(New in v2.0.0)*
For comprehensive distributed tracing, register the `TraceIdFilter` to automatically manage trace IDs across your entire request lifecycle:
**Step 1: Register the Filter**
```java
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<TraceIdFilter> traceIdFilter() {
FilterRegistrationBean<TraceIdFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new TraceIdFilter());
registration.addUrlPatterns("/*");
registration.setOrder(Ordered.HIGHEST_PRECEDENCE);
return registration;
}
}
Step 2: Automatic Benefits
Once registered, the filter:
- Generates a unique UUID for each incoming request
- Stores it in SLF4J MDC (available for logging)
- Makes it available to
ApiResponseautomatically - Cleans up MDC after request completion
Step 3: Trace ID in Logs
All your log statements automatically include the trace ID:
@Service
@Slf4j
public class UserService {
public User findById(Long id) {
log.info("Finding user by ID: {}", id); // Trace ID automatically included
// ... business logic
log.debug("User retrieved from database");
return user;
}
}Log Output (with Logback configuration):
2026-02-06 10:30:45.123 [a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d] INFO c.e.s.UserService - Finding user by ID: 123
2026-02-06 10:30:45.234 [a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d] DEBUG c.e.s.UserService - User retrieved from database
Logback Configuration (logback-spring.xml):
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-DD HH:mm:ss.SSS} [%X{traceId}] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</configuration>The trace ID is resolved with the following priority:
- Explicit Parameter - If you explicitly set a trace ID via builder
- MDC Context - If
TraceIdFilterhas set a trace ID in MDC - Auto-Generated - Falls back to a random UUID
// Priority 1: Explicit trace ID
UUID customId = UUID.randomUUID();
ApiResponse.<User>builder()
.traceId(customId) // This takes highest priority
.status(200)
.message("User found")
.content(user)
.build();
// Priority 2: From MDC (when TraceIdFilter is active)
// Automatically uses the filter-generated trace ID
// Priority 3: Auto-generated (when no filter and no explicit ID)
// Falls back to random UUIDScenario: Client β API Gateway β User Service β Database
// API Gateway receives request with trace ID
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/{id}")
public ResponseEntity<ApiResponse<User>> getUser(@PathVariable Long id) {
// TraceIdFilter already set MDC with trace ID
log.info("Received request for user: {}", id);
User user = userService.findById(id);
// Response automatically includes the same trace ID
return ApiResponse.success("User found", user);
}
}
@Service
@Slf4j
public class UserService {
public User findById(Long id) {
// All logs automatically include trace ID from MDC
log.info("Finding user in database: {}", id);
User user = userRepository.findById(id)
.orElseThrow(() -> {
log.warn("User not found: {}", id);
return new ResourceNotFoundException("User", id);
});
log.info("User found: {}", user.getEmail());
return user;
}
}Complete Request Flow:
1. Request arrives β TraceIdFilter generates UUID: a1b2c3d4-...
2. MDC.put("traceId", "a1b2c3d4-...")
3. Controller logs: [a1b2c3d4-...] INFO - Received request for user: 123
4. Service logs: [a1b2c3d4-...] INFO - Finding user in database: 123
5. Service logs: [a1b2c3d4-...] INFO - User found: john@example.com
6. Response includes: "traceId": "a1b2c3d4-..."
7. MDC.clear() in finally block
For microservices architectures, propagate the trace ID to downstream services:
@Service
@RequiredArgsConstructor
public class OrderService {
private final RestTemplate restTemplate;
public Order createOrder(OrderDto dto) {
// Get trace ID from MDC
String traceId = MDC.get("traceId");
// Create headers with trace ID
HttpHeaders headers = new HttpHeaders();
headers.set("X-Trace-Id", traceId);
headers.setContentType(MediaType.APPLICATION_JSON);
// Call downstream service
HttpEntity<OrderDto> request = new HttpEntity<>(dto, headers);
ResponseEntity<ApiResponse<Order>> response = restTemplate.exchange(
"http://payment-service/api/payments",
HttpMethod.POST,
request,
new ParameterizedTypeReference<ApiResponse<Order>>() {}
);
return response.getBody().getContent();
}
}Downstream Service (Payment Service):
@RestController
@RequestMapping("/api/payments")
public class PaymentController {
@PostMapping
public ResponseEntity<ApiResponse<Payment>> processPayment(
@RequestHeader(value = "X-Trace-Id", required = false) String incomingTraceId,
@RequestBody PaymentDto dto) {
// Use incoming trace ID if provided
if (incomingTraceId != null) {
MDC.put("traceId", incomingTraceId);
}
Payment payment = paymentService.process(dto);
return ApiResponse.created("Payment processed", payment);
}
}Unit Tests:
@Test
void testTraceIdInResponse() {
// Set trace ID in MDC (simulating TraceIdFilter)
String expectedTraceId = UUID.randomUUID().toString();
MDC.put("traceId", expectedTraceId);
try {
ApiResponse<User> response = ApiResponse.<User>builder()
.status(200)
.message("Success")
.content(user)
.build();
assertEquals(UUID.fromString(expectedTraceId), response.getTraceId());
} finally {
MDC.clear();
}
}Integration Tests:
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class UserControllerIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
void testTraceIdInApiResponse() {
ResponseEntity<ApiResponse<User>> response = restTemplate.exchange(
"/api/users/1",
HttpMethod.GET,
null,
new ParameterizedTypeReference<ApiResponse<User>>() {}
);
assertNotNull(response.getBody().getTraceId());
// Trace ID should be in response
}
}- Simplified Debugging - Track requests across multiple services with a single ID
- Log Correlation - All logs for a request share the same trace ID
- Performance Monitoring - Identify slow requests by trace ID
- Error Investigation - Quickly find all logs related to a failed request
- Distributed Systems - Essential for microservices architecture
- Production Ready - No memory leaks with automatic MDC cleanup
For better interoperability with industry-standard distributed tracing systems, you can extend TraceIdFilter to support incoming trace ID headers and add trace IDs to response headers:
package io.github.pasinduog.filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.MDC;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.UUID;
/**
* Enhanced TraceIdFilter with industry-standard header support.
* Supports incoming trace IDs from common headers and propagates to response.
*/
public class EnhancedTraceIdFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
UUID traceId = extractOrGenerateTraceId(request);
try {
request.setAttribute("traceId", traceId);
MDC.put("traceId", traceId.toString());
// Add trace ID to response header for downstream services
response.setHeader("X-Trace-Id", traceId.toString());
filterChain.doFilter(request, response);
} finally {
MDC.clear();
}
}
/**
* Extracts trace ID from standard headers or generates a new one.
* Checks headers in order of priority:
* 1. X-Trace-Id (custom)
* 2. X-Request-ID (common)
* 3. X-B3-TraceId (Zipkin)
* 4. traceparent (W3C Trace Context)
*/
private UUID extractOrGenerateTraceId(HttpServletRequest request) {
// Check X-Trace-Id header
String traceId = request.getHeader("X-Trace-Id");
// Check X-Request-ID header
if (traceId == null) {
traceId = request.getHeader("X-Request-ID");
}
// Check X-B3-TraceId header (Zipkin format)
if (traceId == null) {
traceId = request.getHeader("X-B3-TraceId");
}
// Check W3C traceparent header
if (traceId == null) {
String traceparent = request.getHeader("traceparent");
if (traceparent != null) {
// Format: 00-{trace-id}-{span-id}-{flags}
String[] parts = traceparent.split("-");
if (parts.length >= 2) {
traceId = parts[1]; // Extract trace-id
}
}
}
// Try to parse as UUID, or generate new one
if (traceId != null) {
try {
return UUID.fromString(traceId);
} catch (IllegalArgumentException e) {
// Invalid UUID format, generate new one
}
}
return UUID.randomUUID();
}
}Register the Enhanced Filter:
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<EnhancedTraceIdFilter> traceIdFilter() {
FilterRegistrationBean<EnhancedTraceIdFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new EnhancedTraceIdFilter());
registration.addUrlPatterns("/*");
registration.setOrder(Ordered.HIGHEST_PRECEDENCE);
return registration;
}
}Benefits of Enhanced Filter:
- β Accepts trace IDs from upstream services (API Gateway, Load Balancer)
- β Supports multiple industry-standard header formats
- β Adds trace ID to response headers for downstream services
- β Maintains compatibility with W3C, Zipkin, and custom formats
- β Graceful fallback to UUID generation
Testing with cURL:
# Send request with trace ID in header
curl -H "X-Trace-Id: 12345678-1234-1234-1234-123456789012" \
http://localhost:8080/api/users/1
# Check response header - trace ID is propagated
HTTP/1.1 200 OK
X-Trace-Id: 12345678-1234-1234-1234-123456789012
# Success response body (no traceId field in ApiResponse)
{
"status": 200,
"message": "User found",
"content": {...},
"timestamp": "2026-02-06T10:30:45.123Z"
}
# Error responses include traceId in body (ProblemDetail format)
# For errors, the traceId appears in the response JSON:
{
"type": "about:blank",
"title": "Not Found",
"status": 404,
"detail": "User not found",
"traceId": "12345678-1234-1234-1234-123456789012",
"timestamp": "2026-02-06T10:30:45.123Z"
}Note:
- Success responses (ApiResponse) do NOT have
traceIdin the response body - Error responses (ProblemDetail) DO include
traceIdfor debugging - Both success and error responses can have
X-Trace-Idheader if TraceIdFilter is configured
With data:
@GetMapping
public ResponseEntity<ApiResponse<List<User>>> getAllUsers() {
List<User> users = userService.findAll();
return ApiResponse.success("Users retrieved successfully", users);
}Without data:
@DeleteMapping("/{id}")
public ResponseEntity<ApiResponse<Void>> deleteUser(@PathVariable Long id) {
userService.delete(id);
return ApiResponse.success("User deleted successfully");
}@PostMapping
public ResponseEntity<ApiResponse<User>> createUser(@RequestBody UserDto dto) {
User newUser = userService.create(dto);
return ApiResponse.created("User created successfully", newUser);
}Without data:
@GetMapping("/health")
public ResponseEntity<ApiResponse<Void>> healthCheck() {
if (!service.isHealthy()) {
return ApiResponse.status("Service unavailable", HttpStatus.SERVICE_UNAVAILABLE);
}
return ApiResponse.success("Service is healthy");
}With data:
@PutMapping("/{id}")
public ResponseEntity<ApiResponse<User>> updateUser(
@PathVariable Long id,
@RequestBody UserDto dto) {
User updated = userService.update(id, dto);
return ApiResponse.status("User updated", updated, HttpStatus.OK);
}@RestController
@RequestMapping("/api/products")
@RequiredArgsConstructor
@Slf4j
public class ProductController {
private final ProductService productService;
@GetMapping
public ResponseEntity<ApiResponse<Page<Product>>> getAllProducts(
@PageableDefault(size = 20, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable) {
Page<Product> products = productService.findAll(pageable);
return ApiResponse.success("Products retrieved successfully", products);
}
@GetMapping("/{id}")
public ResponseEntity<ApiResponse<Product>> getProduct(@PathVariable Long id) {
Product product = productService.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Product", id));
return ApiResponse.success("Product found", product);
}
@PostMapping
public ResponseEntity<ApiResponse<Product>> createProduct(
@Valid @RequestBody ProductDto dto) {
Product product = productService.create(dto);
log.info("Product created with ID: {}", product.getId());
return ApiResponse.created("Product created successfully", product);
}
@PutMapping("/{id}")
public ResponseEntity<ApiResponse<Product>> updateProduct(
@PathVariable Long id,
@Valid @RequestBody ProductDto dto) {
Product product = productService.update(id, dto);
return ApiResponse.success("Product updated successfully", product);
}
@DeleteMapping("/{id}")
public ResponseEntity<ApiResponse<Void>> deleteProduct(@PathVariable Long id) {
productService.delete(id);
return ApiResponse.success("Product deleted successfully");
}
@PatchMapping("/{id}/status")
public ResponseEntity<ApiResponse<Product>> updateStatus(
@PathVariable Long id,
@RequestParam ProductStatus status) {
Product product = productService.updateStatus(id, status);
return ApiResponse.success("Product status updated", product);
}
}@RestController
@RequestMapping("/api/files")
@RequiredArgsConstructor
public class FileUploadController {
private final FileStorageService fileService;
@PostMapping("/upload")
public ResponseEntity<ApiResponse<FileMetadata>> uploadFile(
@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
throw new InvalidFileException("File cannot be empty");
}
if (file.getSize() > 10 * 1024 * 1024) { // 10MB limit
throw new FileTooLargeException("File size exceeds 10MB limit");
}
FileMetadata metadata = fileService.store(file);
return ApiResponse.created("File uploaded successfully", metadata);
}
@GetMapping("/{id}/download")
public ResponseEntity<Resource> downloadFile(@PathVariable String id) {
FileData fileData = fileService.load(id);
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(fileData.getContentType()))
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + fileData.getFilename() + "\"")
.body(fileData.getResource());
}
}
// Custom exceptions
public class InvalidFileException extends ApiException {
public InvalidFileException(String message) {
super(message, HttpStatus.BAD_REQUEST);
}
}
public class FileTooLargeException extends ApiException {
public FileTooLargeException(String message) {
super(message, HttpStatus.PAYLOAD_TOO_LARGE);
}
}@RestController
@RequestMapping("/api/reports")
@RequiredArgsConstructor
public class ReportController {
private final ReportService reportService;
@PostMapping("/generate")
public ResponseEntity<ApiResponse<ReportJob>> generateReport(
@Valid @RequestBody ReportRequest request) {
ReportJob job = reportService.submitJob(request);
return ApiResponse.status(
"Report generation started. Check status at /api/reports/" + job.getId(),
job,
HttpStatus.ACCEPTED
);
}
@GetMapping("/{jobId}/status")
public ResponseEntity<ApiResponse<ReportJobStatus>> getStatus(@PathVariable String jobId) {
ReportJobStatus status = reportService.getJobStatus(jobId);
return switch (status.getState()) {
case COMPLETED -> ApiResponse.success("Report is ready", status);
case FAILED -> ApiResponse.status("Report generation failed", status, HttpStatus.INTERNAL_SERVER_ERROR);
case PROCESSING -> ApiResponse.status("Report is being generated", status, HttpStatus.ACCEPTED);
default -> ApiResponse.status("Report is queued", status, HttpStatus.ACCEPTED);
};
}
}@RestController
@RequestMapping("/api/search")
@RequiredArgsConstructor
public class SearchController {
private final SearchService searchService;
@GetMapping("/products")
public ResponseEntity<ApiResponse<SearchResults<Product>>> searchProducts(
@RequestParam(required = false) String query,
@RequestParam(required = false) BigDecimal minPrice,
@RequestParam(required = false) BigDecimal maxPrice,
@RequestParam(required = false) List<String> categories,
@RequestParam(required = false) Boolean inStock,
Pageable pageable) {
SearchCriteria criteria = SearchCriteria.builder()
.query(query)
.minPrice(minPrice)
.maxPrice(maxPrice)
.categories(categories)
.inStock(inStock)
.build();
SearchResults<Product> results = searchService.search(criteria, pageable);
String message = String.format("Found %d results", results.getTotalElements());
return ApiResponse.success(message, results);
}
}@RestController
@RequestMapping("/api/batch")
@RequiredArgsConstructor
public class BatchOperationController {
private final BatchService batchService;
@PostMapping("/users/import")
public ResponseEntity<ApiResponse<BatchResult>> importUsers(
@RequestBody List<@Valid UserImportDto> users) {
if (users.isEmpty()) {
throw new InvalidRequestException("User list cannot be empty");
}
if (users.size() > 1000) {
throw new BatchTooLargeException("Maximum 1000 users per batch");
}
BatchResult result = batchService.importUsers(users);
String message = String.format(
"Batch completed: %d successful, %d failed",
result.getSuccessCount(),
result.getFailureCount()
);
return ApiResponse.success(message, result);
}
@DeleteMapping("/users")
public ResponseEntity<ApiResponse<BatchDeleteResult>> deleteUsers(
@RequestBody List<Long> userIds) {
BatchDeleteResult result = batchService.deleteUsers(userIds);
return ApiResponse.success(
String.format("Deleted %d users", result.getDeletedCount()),
result
);
}
}@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class HealthController {
private final DatabaseHealthChecker dbHealthChecker;
private final CacheHealthChecker cacheHealthChecker;
private final ExternalApiHealthChecker apiHealthChecker;
@GetMapping("/health")
public ResponseEntity<ApiResponse<HealthStatus>> health() {
HealthStatus status = HealthStatus.builder()
.database(dbHealthChecker.check())
.cache(cacheHealthChecker.check())
.externalApi(apiHealthChecker.check())
.timestamp(Instant.now())
.build();
HttpStatus httpStatus = status.isHealthy() ? HttpStatus.OK : HttpStatus.SERVICE_UNAVAILABLE;
String message = status.isHealthy() ? "All systems operational" : "Some systems are down";
return ApiResponse.status(message, status, httpStatus);
}
@GetMapping("/metrics")
public ResponseEntity<ApiResponse<SystemMetrics>> metrics() {
SystemMetrics metrics = SystemMetrics.builder()
.uptime(ManagementFactory.getRuntimeMXBean().getUptime())
.memoryUsage(Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory())
.activeThreads(Thread.activeCount())
.timestamp(Instant.now())
.build();
return ApiResponse.success("System metrics retrieved", metrics);
}
}Version 2.0.0 includes a comprehensive GlobalExceptionHandler with 10 built-in exception handlers using Spring Boot's ProblemDetail (RFC 9457) for standardized error responses:
Handled Exception Types:
- β General Exceptions (HTTP 500)
- β
Validation Errors (HTTP 400) -
@Validannotation failures - β Type Mismatches (HTTP 400) - Wrong parameter types
- β Malformed JSON (HTTP 400) - Invalid request body (New in v2.0.0)
- β
Missing Parameters (HTTP 400) - Required
@RequestParammissing (New in v2.0.0) - β 404 Not Found (HTTP 404) - Missing endpoints/resources (New in v2.0.0)
- β 405 Method Not Allowed (HTTP 405) - Wrong HTTP method (New in v2.0.0)
- β 415 Unsupported Media Type (HTTP 415) - Invalid Content-Type (New in v2.0.0)
- β Null Pointer Exceptions (HTTP 500)
- β Custom ApiExceptions - Domain-specific business logic errors
package io.github.pasinduog.exception;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.resource.NoResourceFoundException;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
// 10 comprehensive exception handlers included
// See full implementation in the source code
private String getOrGenerateTraceId() {
String traceId = MDC.get("traceId");
if (traceId == null) {
traceId = UUID.randomUUID().toString();
MDC.put("traceId", traceId);
}
return traceId;
}
@ExceptionHandler(Exception.class)
public ProblemDetail handleAllExceptions(Exception ex) {
String traceId = getOrGenerateTraceId();
log.error("[TraceID: {}] An unexpected error occurred: ", traceId, ex);
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(
HttpStatus.INTERNAL_SERVER_ERROR,
"Internal Server Error. Please contact technical support"
);
problemDetail.setProperty("traceId", traceId);
problemDetail.setProperty("timestamp", Instant.now());
return problemDetail;
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ProblemDetail handleValidationExceptions(MethodArgumentNotValidException ex) {
String traceId = getOrGenerateTraceId();
Map<String, String> errorMessage = new HashMap<>();
for (FieldError fieldError : ex.getBindingResult().getFieldErrors()) {
errorMessage.merge(fieldError.getField(), fieldError.getDefaultMessage(),
(msg1, msg2) -> msg1 + "; " + msg2);
}
log.warn("[TraceID: {}] Validation error: {}", traceId, errorMessage);
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(
HttpStatus.BAD_REQUEST,
"Validation Failed"
);
problemDetail.setProperty("errors", errorMessage);
problemDetail.setProperty("traceId", traceId);
problemDetail.setProperty("timestamp", Instant.now());
return problemDetail;
}
@ExceptionHandler(NullPointerException.class)
public ProblemDetail handleNullPointerExceptions(NullPointerException ex) {
String traceId = getOrGenerateTraceId();
log.error("[TraceID: {}] Null pointer exception occurred: ", traceId, ex);
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(
HttpStatus.INTERNAL_SERVER_ERROR,
"A null pointer exception occurred."
);
problemDetail.setProperty("traceId", traceId);
problemDetail.setProperty("timestamp", Instant.now());
return problemDetail;
}
}The GlobalExceptionHandler provides:
- π‘οΈ ProblemDetail RFC 9457 - Latest standard error format (supersedes RFC 7807)
- β
Validation Error Handling - Automatic
@Validannotation support - π Comprehensive Logging - SLF4J integration with trace IDs
- β° Automatic Timestamps - On all error responses
- π Trace ID Generation - Automatic UUID generation for all errors
- π Null Pointer Protection - Dedicated NullPointerException handling
- π 10 Exception Handlers - Covers all common error scenarios
Example Validation Error Response:
{
"type": "about:blank",
"title": "Bad Request",
"status": 400,
"detail": "Validation Failed",
"errors": {
"email": "must be a well-formed email address",
"name": "must not be blank"
},
"timestamp": "2026-02-02T10:30:45.123Z"
}You can also create custom exception handlers using ApiResponse:
@ControllerAdvice
public class CustomExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ApiResponse<Void>> handleNotFound(ResourceNotFoundException ex) {
return ApiResponse.status(ex.getMessage(), HttpStatus.NOT_FOUND);
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ApiResponse<Void>> handleBadRequest(IllegalArgumentException ex) {
return ApiResponse.status("Invalid request: " + ex.getMessage(), HttpStatus.BAD_REQUEST);
}
}| Method | Parameters | Return Type | HTTP Status | Description |
|---|---|---|---|---|
success(String message) |
message | ResponseEntity<ApiResponse<Void>> |
200 OK | Success without data |
success(String message, T data) |
message, data | ResponseEntity<ApiResponse<T>> |
200 OK | Success with data |
created(String message, T data) |
message, data | ResponseEntity<ApiResponse<T>> |
201 CREATED | Resource creation |
status(String message, HttpStatus status) |
message, status | ResponseEntity<ApiResponse<Void>> |
Custom | Custom status without data |
status(String message, T data, HttpStatus status) |
message, data, status | ResponseEntity<ApiResponse<T>> |
Custom | Custom status with data |
| Field | Type | Description |
|---|---|---|
status |
Integer |
HTTP status code (e.g., 200, 201, 404) |
traceId |
UUID |
Unique identifier for request tracing and log correlation - Enhanced in v2.0.0 with MDC integration |
message |
String |
Human-readable message describing the response |
content |
T (Generic) |
Response payload (can be any type or null) |
timestamp |
Instant |
ISO-8601 formatted UTC timestamp (auto-generated) |
All success responses follow this consistent structure:
{
"status": 200,
"message": "string",
"content": {},
"timestamp": "2026-02-06T10:30:45.123456Z"
}Note: traceId is NOT included in success responses. Trace IDs are only added to error responses (ProblemDetail format) by the GlobalExceptionHandler for debugging purposes.
Single Object:
{
"status": 200,
"message": "Product found",
"content": {
"id": 1,
"name": "Laptop",
"price": 999.99
},
"timestamp": "2026-02-01T10:30:45.123Z"
}Array/List:
{
"status": 200,
"message": "Products retrieved",
"content": [
{"id": 1, "name": "Laptop"},
{"id": 2, "name": "Mouse"}
],
"timestamp": "2026-02-01T10:30:45.123Z"
}No Content (Void):
{
"status": 200,
"message": "Product deleted successfully",
"timestamp": "2026-02-01T10:30:45.123Z"
}Error Response (ProblemDetail format with traceId):
{
"type": "about:blank",
"title": "Not Found",
"status": 404,
"detail": "Product not found with ID: 123",
"traceId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-02-01T10:30:45.123Z"
}// β
Use success() for standard operations
return ApiResponse.success("Retrieved", data);
// β
Use created() for resource creation
return ApiResponse.created("Created", newResource);
// β
Use status() for custom status codes
return ApiResponse.status("Accepted", HttpStatus.ACCEPTED);// β
Good - Descriptive and specific
return ApiResponse.success("User profile updated successfully", user);
// β Avoid - Too generic
return ApiResponse.success("Success", user);// Specific types
ResponseEntity<ApiResponse<User>> getUser();
ResponseEntity<ApiResponse<List<Product>>> getProducts();
ResponseEntity<ApiResponse<Map<String, Object>>> getMetadata();
ResponseEntity<ApiResponse<Void>> deleteResource();Version 1.2.0+ includes an abstract ApiException class for creating domain-specific exceptions. The built-in GlobalExceptionHandler automatically handles them with the correct HTTP status:
// Define custom exceptions
public class ResourceNotFoundException extends ApiException {
public ResourceNotFoundException(String resource, Long id) {
super(String.format("%s not found with ID: %d", resource, id), HttpStatus.NOT_FOUND);
}
}
public class InsufficientBalanceException extends ApiException {
public InsufficientBalanceException(String accountId) {
super("Insufficient balance in account: " + accountId, HttpStatus.PAYMENT_REQUIRED);
}
}
// Use them in your service/controller
@GetMapping("/{id}")
public ResponseEntity<ApiResponse<User>> getUser(@PathVariable Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("User", id));
return ApiResponse.success("User found", user);
}Benefits:
- β
No need to manually create
@ExceptionHandlermethods - β Automatic RFC 9457 ProblemDetail formatting
- β Type-safe with compile-time checking
- β Clean, readable code
You can still create additional custom exception handlers if needed:
@ControllerAdvice
public class CustomExceptionHandler {
@ExceptionHandler(ThirdPartyApiException.class)
public ProblemDetail handleThirdPartyError(ThirdPartyApiException ex) {
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
HttpStatus.BAD_GATEWAY, ex.getMessage()
);
problem.setProperty("timestamp", Instant.now());
return problem;
}
}@RestController
@RequestMapping("/api/products")
public class ProductController {
@GetMapping("/{id}") // 200 OK
public ResponseEntity<ApiResponse<Product>> get(@PathVariable Long id) {
return ApiResponse.success("Product found", productService.findById(id));
}
@PostMapping // 201 CREATED
public ResponseEntity<ApiResponse<Product>> create(@RequestBody ProductDto dto) {
return ApiResponse.created("Product created", productService.create(dto));
}
@PutMapping("/{id}") // 200 OK
public ResponseEntity<ApiResponse<Product>> update(
@PathVariable Long id, @RequestBody ProductDto dto) {
return ApiResponse.success("Product updated", productService.update(id, dto));
}
@DeleteMapping("/{id}") // 200 OK
public ResponseEntity<ApiResponse<Void>> delete(@PathVariable Long id) {
productService.delete(id);
return ApiResponse.success("Product deleted");
}
}@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
void shouldReturnUserSuccessfully() throws Exception {
User user = new User(1L, "John Doe", "john@example.com");
when(userService.findById(1L)).thenReturn(user);
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value(200))
.andExpect(jsonPath("$.message").value("User retrieved successfully"))
.andExpect(jsonPath("$.data.id").value(1))
.andExpect(jsonPath("$.data.name").value("John Doe"))
.andExpect(jsonPath("$.traceId").exists())
.andExpect(jsonPath("$.timestamp").exists());
}
@Test
void shouldReturnCreatedStatus() throws Exception {
User newUser = new User(1L, "Jane Doe", "jane@example.com");
when(userService.create(any())).thenReturn(newUser);
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"Jane Doe\",\"email\":\"jane@example.com\"}"))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.status").value(201))
.andExpect(jsonPath("$.message").value("User created successfully"))
.andExpect(jsonPath("$.data.id").value(1))
.andExpect(jsonPath("$.traceId").exists());
}
@Test
void shouldReturnValidationErrors() throws Exception {
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"\",\"email\":\"invalid\"}"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.status").value(400))
.andExpect(jsonPath("$.detail").value("Validation Failed"))
.andExpect(jsonPath("$.errors.name").exists())
.andExpect(jsonPath("$.errors.email").exists())
.andExpect(jsonPath("$.timestamp").exists());
}
@Test
void shouldHandleCustomException() throws Exception {
when(userService.findById(999L))
.thenThrow(new ResourceNotFoundException("User", 999L));
mockMvc.perform(get("/api/users/999"))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.status").value(404))
.andExpect(jsonPath("$.detail").value("User not found with ID: 999"))
.andExpect(jsonPath("$.timestamp").exists());
}
}@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class UserControllerIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
void shouldGetUser() {
ResponseEntity<ApiResponse> response = restTemplate.getForEntity(
"/api/users/1", ApiResponse.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody().getStatus()).isEqualTo(200);
assertThat(response.getBody().getMessage()).contains("User");
assertThat(response.getBody().getTraceId()).isNotNull();
assertThat(response.getBody().getTimestamp()).isNotNull();
}
@Test
void shouldCreateUser() {
UserDto newUser = new UserDto("Jane Doe", "jane@example.com");
ResponseEntity<ApiResponse> response = restTemplate.postForEntity(
"/api/users", newUser, ApiResponse.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
assertThat(response.getBody().getStatus()).isEqualTo(201);
}
}@ExtendWith(MockitoExtension.class)
class CustomExceptionTest {
@Test
void shouldThrowResourceNotFoundException() {
ResourceNotFoundException exception =
new ResourceNotFoundException("User", 123L);
assertThat(exception.getMessage()).isEqualTo("User not found with ID: 123");
assertThat(exception.getStatus()).isEqualTo(HttpStatus.NOT_FOUND);
}
}If you adapt the library for reactive applications:
@WebFluxTest(UserController.class)
class UserControllerWebFluxTest {
@Autowired
private WebTestClient webTestClient;
@MockBean
private UserService userService;
@Test
void shouldReturnUser() {
User user = new User(1L, "John Doe", "john@example.com");
when(userService.findById(1L)).thenReturn(Mono.just(user));
webTestClient.get()
.uri("/api/users/1")
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$.status").isEqualTo(200)
.jsonPath("$.data.name").isEqualTo("John Doe")
.jsonPath("$.traceId").exists();
}
} User newUser = new User(1L, "Jane Doe", "jane@example.com");
when(userService.create(any())).thenReturn(newUser);
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"Jane Doe\",\"email\":\"jane@example.com\"}"))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.status").value(201))
.andExpect(jsonPath("$.message").value("User created successfully"))
.andExpect(jsonPath("$.data.id").value(1))
.andExpect(jsonPath("$.traceId").exists());
}
}
## ποΈ Architecture & Design Principles
### Thread Safety & Immutability
The `ApiResponse<T>` class is designed with **immutability** at its core:
- All fields are declared as `final`
- No setter methods exist (only getters)
- Uses a custom inner Builder class for object construction
- Thread-safe by design - can be safely shared across threads
```java
// Once created, the response cannot be modified
ApiResponse<User> response = new ApiResponse.ApiResponseBuilder<User>()
.message("Success")
.content(user)
.build();
// This is thread-safe and can be safely cached or shared
The library uses provided scope for Spring Boot:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>provided</scope> <!-- Will not bloat your application -->
</dependency>Benefits:
- β No Dependency Conflicts - Uses your application's existing Spring Boot version
- β Zero External Dependencies - Pure Java implementation, no Lombok or other libraries required
- β Zero Bloat - Adds only ~10KB to your application
- β Version Flexibility - Compatible with Spring Boot 3.2.0 - 4.0.2 and Java 17+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Spring Boot Application Starts β
βββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Reads META-INF/spring/ β
β org.springframework.boot.autoconfigure β
β .AutoConfiguration.imports β
βββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Loads ApiResponseAutoConfiguration β
βββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Registers GlobalExceptionHandler Bean β
β (as @RestControllerAdvice) β
βββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Ready to Handle Exceptions Automatically β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- Factory Pattern - Static factory methods (
success(),created(),status()) - Builder Pattern - Custom inner Builder class for flexible object construction
- Template Method Pattern -
ApiExceptionabstract class for custom exceptions - Advisor Pattern -
GlobalExceptionHandlerwith@RestControllerAdvice
Works seamlessly with SpringDoc OpenAPI:
@Operation(summary = "Get user by ID", description = "Returns a single user")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "User found"),
@ApiResponse(responseCode = "404", description = "User not found")
})
@GetMapping("/{id}")
public ResponseEntity<ApiResponse<User>> getUser(@PathVariable Long id) {
User user = userService.findById(id);
return ApiResponse.success("User retrieved successfully", user);
}Add SpringDoc dependency:
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.3.0</version>
</dependency>| Library Version | Java Version | Spring Boot Version | Status |
|---|---|---|---|
| 2.0.0 | 17, 21+ | 3.2.0 - 4.0.2 | β Tested |
| 1.3.0 | 17, 21+ | 3.2.0 - 4.0.2 | β Tested |
| 1.2.0 | 17, 21+ | 3.2.0+ | β Tested |
| 1.1.0 | 17, 21+ | 3.2.0+ | β Tested |
| 1.0.0 | 17, 21+ | 3.2.0+ | β Tested |
Minimum Requirements:
- Java: 17 or higher
- Spring Boot: 3.2.0 or higher (tested up to 4.0.2)
- Dependencies: None (pure Java implementation)
Recommended:
- Java: 21 (LTS)
- Spring Boot: 3.4.x or 4.0.x (fully compatible with Spring Boot 4)
β Full Spring Boot 4.0.2 Compatibility
- The library has been tested and verified to work with Spring Boot 4.0.2
- All features including auto-configuration work seamlessly
- No breaking changes when upgrading from Spring Boot 3.x to 4.x
- Uses provided scope dependencies to avoid version conflicts
| Framework | Supported | Notes |
|---|---|---|
| Spring Boot 4.x | β Yes | Full support with version 4.0.2 |
| Spring Boot 3.x | β Yes | Full support with auto-configuration |
| Spring Boot 2.x | β No | Use Spring Boot 3.x+ |
| Spring WebFlux | Manual adaptation required | |
| Micronaut | β No | Spring-specific features used |
| Quarkus | β No | Spring-specific features used |
| Build Tool | Supported | Configuration |
|---|---|---|
| Maven | β Yes | Native support |
| Gradle | β Yes | Groovy & Kotlin DSL |
| Gradle (Groovy) | β Yes | implementation 'io.github.pasinduog:api-response:2.0.0' |
| Gradle (Kotlin) | β Yes | implementation("io.github.pasinduog:api-response:2.0.0") |
Problem: Exceptions are not being caught by the GlobalExceptionHandler.
Solution:
- Ensure you're using version 1.3.0+ with auto-configuration
- Check that auto-configuration is not excluded
- Verify Spring Boot version is 3.2.0+
// Verify auto-configuration is active
@SpringBootApplication
// Do NOT exclude ApiResponseAutoConfiguration
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}Problem: Some fields like status, message, or content are missing in JSON responses.
Solution:
- The library uses
@JsonInclude(JsonInclude.Include.NON_NULL)to exclude null fields - This is intentional behavior - only non-null fields are included in the response
- To include all fields, you would need to customize Jackson's configuration in your application
Problem: GlobalExceptionHandler is not being picked up automatically.
Solution:
- Ensure you're using Spring Boot 3.2.0 or higher
- Check that the library JAR is on the classpath
- Verify
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.importsexists in the JAR - Check application logs for auto-configuration reports
.status(200)
.traceId(UUID.randomUUID()) // Must set manually
.message("Success")
.content(data)
.build());
- For **error responses**, trace IDs are **automatically generated** by GlobalExceptionHandler *(v2.0.0+)*
- All error logs and responses have matching trace IDs for easy correlation
#### 4. Dependency Conflicts
**Problem:** Version conflicts with Spring Boot.
**Solution:**
- The library uses `provided` scope for Spring Boot - it won't conflict
- Ensure your application has Spring Boot Web dependency:
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
Problem: Custom ApiException subclass not returning ProblemDetail.
Solution:
- Ensure your exception extends
ApiException - Verify the exception is actually being thrown
- Check GlobalExceptionHandler is registered
// β
Correct
public class MyException extends ApiException {
public MyException(String message) {
super(message, HttpStatus.BAD_REQUEST);
}
}
// β Wrong - must extend ApiException
public class MyException extends RuntimeException {
// ...
}Problem: Timestamp format not as expected.
Solution:
- The library uses
Instant(UTC) by default - Configure Jackson if you need different format:
@Configuration
public class JacksonConfig {
@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
return mapper;
}
}Problem: Auto-configuration doesn't work after upgrading to 1.3.0.
Solution:
- Verify you're using Spring Boot 3.x (not 2.x)
- Check the JAR includes
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports - Clear Maven/Gradle cache and rebuild:
# Maven
mvn clean install
# Gradle
./gradlew clean build --refresh-dependenciesProblem: Fields are not serializing as expected or timestamp format is wrong.
Solution:
- The library uses Jackson's
@JsonInclude(NON_NULL)by default - Ensure you have
jackson-datatype-jsr310for Java 8+ date/time support (included in Spring Boot) - Configure Jackson if needed:
@Configuration
public class JacksonConfig {
@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
return mapper;
}
}Problem: TraceId appears as an object instead of string.
Solution:
- This shouldn't happen with standard Jackson configuration
- Verify Jackson version is compatible with Spring Boot
- UUID is serialized as string by default in Jackson
// β
Correct
"traceId": "550e8400-e29b-41d4-a716-446655440000"
// β Wrong (shouldn't happen)
"traceId": {"mostSigBits": 123, "leastSigBits": 456}If you encounter issues not covered here:
- Check the Issues: GitHub Issues
- Review JavaDocs: All classes are fully documented
- Enable Debug Logging:
logging.level.io.github.pasinduog=DEBUG - Open an Issue: Provide minimal reproducible example
Error responses automatically include a traceId (UUID) in the ProblemDetail format for request tracking and debugging. Success responses (ApiResponse) do NOT include traceId in the response body, but you can add trace IDs via headers using the TraceIdFilter.
Key Points:
- β Error responses - traceId is automatically included in ProblemDetail JSON
- β Success responses - traceId is NOT in ApiResponse JSON (use headers instead)
- π Logging - All exceptions are logged with [TraceID: xxx] for correlation
For comprehensive distributed tracing with MDC integration and header propagation, see the Distributed Tracing section above.
Quick Example with TraceIdFilter:
// 1. Register TraceIdFilter for header propagation
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<TraceIdFilter> traceIdFilter() {
FilterRegistrationBean<TraceIdFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new TraceIdFilter());
registration.addUrlPatterns("/*");
registration.setOrder(Ordered.HIGHEST_PRECEDENCE);
return registration;
}
}
// 2. Use in your service (trace ID automatically in logs)
@Service
@Slf4j
public class UserService {
public User createUser(UserDto dto) {
log.info("Creating user with email: {}", dto.getEmail()); // [traceId] in logs
// ... business logic
return user;
}
}For industry-standard header propagation (accepting trace IDs from upstream services), use the Enhanced TraceIdFilter which supports:
X-Trace-IdandX-Request-IDheadersX-B3-TraceId(Zipkin format)traceparent(W3C Trace Context)- Automatic response header injection }
**Response:**
```json
{
"status": 200,
"message": "User created",
"content": {...},
"timestamp": "2026-02-06T10:30:45.123Z"
}
Note: The traceId is in the MDC for logging and can be added to response headers, but is NOT in the ApiResponse JSON body.
Logs (with traceId from MDC):
2026-02-06 10:30:45.123 [550e8400-e29b-41d4-a716-446655440000] INFO c.e.s.UserService - Creating user with email: john@example.com
For more details on MDC integration, propagating trace IDs to downstream services, and end-to-end tracing examples, see the **[Distributed Tracing](#-distributed-tracing-enhanced-in-v200)** section.
Extend the abstract ApiException class to create domain-specific exceptions:
public class ResourceNotFoundException extends ApiException {
public ResourceNotFoundException(String resource, Long id) {
super(String.format("%s not found with ID: %d", resource, id), HttpStatus.NOT_FOUND);
}
}
public class DuplicateResourceException extends ApiException {
public DuplicateResourceException(String message) {
super(message, HttpStatus.CONFLICT);
}
}Then throw them in your code - the GlobalExceptionHandler will automatically convert them to RFC 9457 ProblemDetail responses:
@GetMapping("/{id}")
public ResponseEntity<ApiResponse<User>> getUser(@PathVariable Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("User", id));
return ApiResponse.success("User found", user);
}As of v1.3.0, the GlobalExceptionHandler is automatically configured via Spring Boot Auto-Configuration. No manual setup is required!
Simply add the library dependency, and the exception handler will be active immediately. The auto-configuration mechanism automatically registers the handler when the library is detected on the classpath.
For versions prior to 1.3.0, you needed to ensure component scanning:
@SpringBootApplication
@ComponentScan(basePackages = {"com.yourapp", "io.github.pasinduog.exception"})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}Yes! You can add your own @ControllerAdvice handlers alongside the built-in one:
@ControllerAdvice
public class CustomExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ApiResponse<Void>> handleNotFound(ResourceNotFoundException ex) {
return ApiResponse.status(ex.getMessage(), HttpStatus.NOT_FOUND);
}
@ExceptionHandler(UnauthorizedException.class)
public ProblemDetail handleUnauthorized(UnauthorizedException ex) {
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
HttpStatus.UNAUTHORIZED, ex.getMessage()
);
problem.setProperty("timestamp", Instant.now());
return problem;
}
}As of v1.3.0, you can disable it by excluding the auto-configuration:
@SpringBootApplication(exclude = ApiResponseAutoConfiguration.class)
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}Or in application.properties:
spring.autoconfigure.exclude=io.github.pasinduog.config.ApiResponseAutoConfigurationFor versions prior to 1.3.0, you needed to use component scanning filters:
@SpringBootApplication
@ComponentScan(basePackages = "com.yourapp",
excludeFilters = @ComponentScan.Filter(
type = FilterType.ASSIGNABLE_TYPE,
classes = GlobalExceptionHandler.class
))
public class Application {
// ...
}Configure Jackson's ObjectMapper:
@Configuration
public class JacksonConfig {
@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
return mapper;
}
}The library provides a standard structure. To add custom fields, wrap the response:
public class ExtendedResponse<T> {
private ApiResponse<T> response;
private String requestId;
private Map<String, String> metadata;
// getters and setters
}Currently designed for Spring MVC. For WebFlux, you'd need to adapt it to work with Mono<ResponseEntity<ApiResponse<T>>>.
Use Spring's Page as the data type:
@GetMapping
public ResponseEntity<ApiResponse<Page<User>>> getUsers(Pageable pageable) {
Page<User> users = userService.findAll(pageable);
return ApiResponse.success("Users retrieved", users);
}| Plain ResponseEntity | ApiResponse |
|---|---|
| Inconsistent structure | β Standardized everywhere |
| Manual timestamps | β Automatic |
| No trace IDs | β Built-in UUID trace IDs (v1.2.0) |
| No status in body | β Status code included in response (v1.2.0) |
| Manual exception handling | β Custom ApiException support (v1.2.0) |
| More boilerplate | β Concise factory methods |
| No message field | β Always includes message |
- Response Creation: < 1ms (simple object instantiation with builder pattern)
- Memory Footprint: ~200 bytes per response object (excluding data payload)
- Thread Safety: 100% thread-safe (immutable design with final fields)
- GC Impact: Minimal (uses immutable objects, eligible for quick collection)
- JSON Serialization: Optimized with
@JsonInclude(NON_NULL)to reduce payload size - UUID Generation: Negligible overhead (~0.1ms per UUID using
UUID.randomUUID()) - Timestamp Generation: Negligible overhead (~0.01ms using
Instant.now())
| Operation | Time | Notes |
|---|---|---|
ApiResponse.success() |
~0.5ms | Including UUID and timestamp generation |
ApiResponse.created() |
~0.5ms | Same as success() |
ApiResponse.builder().build() |
~0.3ms | Manual builder without factory methods |
| JSON Serialization (small DTO) | ~1-2ms | Standard Jackson performance |
| GlobalExceptionHandler catch | ~0.1ms | Minimal overhead for exception transformation |
// β
RECOMMENDED - Auto-generates trace ID and timestamp
return ApiResponse.success("User found", user);
// β οΈ AVOID - More verbose, manual field management
return ResponseEntity.ok(ApiResponse.<User>builder()
.status(200)
.traceId(UUID.randomUUID())
.message("User found")
.data(user)
.timestamp(Instant.now())
.build());// β
POST - Use created() for 201
@PostMapping
public ResponseEntity<ApiResponse<User>> create(@RequestBody UserDto dto) {
return ApiResponse.created("User created", userService.create(dto));
}
// β
DELETE - Use success() with no data
@DeleteMapping("/{id}")
public ResponseEntity<ApiResponse<Void>> delete(@PathVariable Long id) {
userService.delete(id);
return ApiResponse.success("User deleted");
}
// β
Custom status - Use status() method
@GetMapping("/health")
public ResponseEntity<ApiResponse<Void>> health() {
return ApiResponse.status("Service degraded", HttpStatus.SERVICE_UNAVAILABLE);
}// β
GOOD - Clear and actionable
return ApiResponse.success("User profile updated successfully", updatedUser);
// β BAD - Too generic
return ApiResponse.success("Success", updatedUser);
// β BAD - Technical jargon
return ApiResponse.success("User entity persisted to database", updatedUser);// β
GOOD - Use custom ApiException
public class InsufficientFundsException extends ApiException {
public InsufficientFundsException(String accountId) {
super("Insufficient funds in account: " + accountId, HttpStatus.PAYMENT_REQUIRED);
}
}
// In your service
if (account.getBalance() < amount) {
throw new InsufficientFundsException(account.getId());
}
// β AVOID - Generic exceptions
throw new RuntimeException("Not enough money");// Log the trace ID from incoming requests
@PostMapping
public ResponseEntity<ApiResponse<User>> createUser(@RequestBody UserDto dto) {
UUID traceId = UUID.randomUUID();
log.info("Processing user creation request, traceId: {}", traceId);
User user = userService.create(dto);
// Create response with same trace ID
ApiResponse<User> response = ApiResponse.<User>builder()
.status(HttpStatus.CREATED.value())
.traceId(traceId)
.message("User created successfully")
.data(user)
.build();
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}// β οΈ CAUTION - Response includes timestamp, making caching difficult
// Consider extracting just the data for caching:
@Cacheable("users")
public User getUserData(Long id) {
return userRepository.findById(id).orElseThrow();
}
@GetMapping("/{id}")
public ResponseEntity<ApiResponse<User>> getUser(@PathVariable Long id) {
// Fresh response wrapper, cached data
return ApiResponse.success("User found", getUserData(id));
}Version 3.0.0 is a MAJOR release with BREAKING CHANGES. Please read carefully.
v2.0.0: Used Lombok with @Builder annotation
v3.0.0: Pure Java with manual builder pattern
Migration Steps:
If you were using the builder directly (rare):
// BEFORE (v2.0.0 - won't work in v3.0.0)
ApiResponse<User> response = ApiResponse.<User>builder()
.status(200)
.message("Success")
.data(user) // Also note: "data" changed to "content"
.build();
// AFTER (v3.0.0)
ApiResponse<User> response = new ApiResponse.ApiResponseBuilder<User>()
.message("Success")
.content(user) // Changed from "data" to "content"
.build();
// Note: status and timestamp are set automaticallyRecommended: Use static factory methods (no changes needed):
// This works in BOTH v2.0.0 and v3.0.0
return ApiResponse.success("User found", user);
return ApiResponse.created("User created", newUser);
return ApiResponse.status("Accepted", HttpStatus.ACCEPTED);v2.0.0: Success responses (ApiResponse) included traceId field
v3.0.0: Success responses NO LONGER include traceId field
Impact:
- Success response JSON no longer has
traceIdfield - Error responses (ProblemDetail) still have
traceId
Before (v2.0.0):
{
"status": 200,
"traceId": "550e8400-e29b-41d4-a716-446655440000",
"message": "Success",
"content": {...},
"timestamp": "2026-02-13T10:30:45.123Z"
}After (v3.0.0):
{
"status": 200,
"message": "Success",
"content": {...},
"timestamp": "2026-02-13T10:30:45.123Z"
}Migration Options:
-
Use MDC for trace IDs (Recommended):
String traceId = MDC.get("traceId"); log.info("Processing request with traceId: {}", traceId);
-
Use Response Headers:
return ResponseEntity.ok() .header("X-Trace-Id", traceId) .body(ApiResponse.success("Success", data));
-
Implement TraceIdFilter: Add trace ID to MDC and response headers automatically
- RFC 9457 (updated from RFC 7807) - Same structure, updated standard
- All static factory methods work the same
- Exception handling unchanged
- Auto-configuration unchanged
- Update dependency to v3.0.0
- If using builder directly: Update to
new ApiResponse.ApiResponseBuilder<T>() - Update client code expecting
traceIdin success responses - Test your application thoroughly
- Update any documentation referencing
traceIdin success responses
Version 2.0.0 was fully backward compatible with v1.3.0 but used Lombok.
Version 1.3.0 introduces auto-configuration. No breaking changes - fully backward compatible.
- β Spring Boot auto-configuration (zero config needed)
- β Enhanced JavaDoc documentation
- β Type mismatch error handler added
- Update dependency version:
<dependency>
<groupId>io.github.pasinduog</groupId>
<artifactId>api-response</artifactId>
<version>2.0.0</version> <!-- Changed from 1.3.0 -->
</dependency>- Remove manual component scanning (optional):
// BEFORE (1.2.0)
@SpringBootApplication
@ComponentScan(basePackages = {"com.yourapp", "io.github.pasinduog.exception"})
public class Application { }
// AFTER (1.3.0) - No need for manual scanning
@SpringBootApplication
public class Application { }- Verify auto-configuration (optional):
# application.properties - Enable debug logging to verify
logging.level.io.github.pasinduog=DEBUGtraceId in ApiResponse, which was later removed in v3.0.0.
Version 1.2.0 adds trace IDs and status fields. Backward compatible with response structure changes.
- β
traceIdfield (UUID) for distributed tracing (Removed in v3.0.0) - β
statusfield (Integer) in response body - β
Custom
ApiExceptionsupport - β
Instanttimestamp (wasLocalDateTime)
-
Update dependency to 1.2.0+
-
Update response assertions in tests:
// Add new field checks
.andExpect(jsonPath("$.status").exists())
.andExpect(jsonPath("$.traceId").exists())- Optional: Create custom exceptions:
public class ResourceNotFoundException extends ApiException {
public ResourceNotFoundException(String resource, Long id) {
super(String.format("%s not found with ID: %d", resource, id), HttpStatus.NOT_FOUND);
}
}Version 1.1.0 adds GlobalExceptionHandler. No breaking changes.
- β
GlobalExceptionHandlerwith RFC 9457 ProblemDetail (latest standard, supersedes RFC 7807) - β Automatic validation error handling
- β SLF4J logging integration with trace IDs
-
Update dependency to 1.1.0+
-
Remove custom exception handlers (optional):
If you were manually handling validation errors, you can now remove that code as it's handled automatically.
The library's GlobalExceptionHandler provides safe defaults, but be mindful:
// β
SAFE - No sensitive data
throw new ApiException("User not found", HttpStatus.NOT_FOUND);
// β οΈ CAUTION - May leak sensitive information
throw new ApiException("Database connection failed: " + sqlException.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
// β
BETTER - Generic message
throw new ApiException("An internal error occurred", HttpStatus.INTERNAL_SERVER_ERROR);Trace IDs are UUIDs and don't contain sensitive information. However:
- Don't log sensitive data alongside trace IDs
- Consider rate limiting to prevent trace ID enumeration
- Rotate logs regularly to limit exposure
// β
SAFE
log.info("User created successfully, traceId: {}", traceId);
// β UNSAFE - Logs password
log.info("User created, traceId: {}, password: {}", traceId, password);The GlobalExceptionHandler never exposes stack traces to clients. Stack traces are:
- β Logged server-side for debugging
- β Never sent in API responses
- β Replaced with generic messages
Validation errors include field names and constraints:
{
"status": 400,
"detail": "Validation Failed",
"errors": {
"email": "must be a well-formed email address",
"password": "must not be blank"
}
}Security Tips:
- β Don't include sensitive field values in error messages
- β Use generic constraint messages for sensitive fields
- β Consider custom validators for sensitive data
public class SensitiveDto {
@NotBlank(message = "Required field is missing") // Generic message
private String creditCardNumber;
@Pattern(regexp = "...", message = "Invalid format") // No details
private String ssn;
}The library doesn't interfere with Spring Security or CORS configuration:
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(csrf -> csrf.disable())
// ... other configurations
return http.build();
}
}The library uses provided scope dependencies:
- β No transitive dependency vulnerabilities
- β Uses your application's Spring Boot version
- β No additional security surface area
Verify with:
mvn dependency:tree -Dincludes=io.github.pasinduog:api-responseRFC 9457 ProblemDetail responses (supersedes RFC 7807) include:
type- URI reference (defaults to "about:blank")title- Short, human-readable summarystatus- HTTP status codedetail- Human-readable explanationinstance- URI reference (not used by default)
Best Practice: Don't include internal system details in error messages.
We welcome and encourage contributions from everyone! π
This project is licensed under the Apache License 2.0, which means:
- β Anyone can contribute - The project is open to all developers
- β Your contributions are protected - No one can claim exclusive ownership of your work
- β You retain copyright - You keep rights to your work while granting usage rights to the project
- β Fair and equal terms - All contributions are made under the same Apache 2.0 terms
By contributing to this project, you agree that:
- Your contributions will be licensed under the Apache License 2.0
- You grant the project maintainers and users a perpetual, worldwide license to use your contributions
- You retain copyright to your contributions
- You confirm that you have the right to submit the contribution and grant these rights
- Your work is protected - No one (including the project maintainer) can claim exclusive ownership of your contributions or publish them elsewhere as their own
β Your Rights:
- You keep copyright to your contributions
- Your name is attributed in the project
- You can use your contributions elsewhere
- You're protected from patent claims
π‘οΈ Project Protection:
- Prevents anyone from copyrighting the project - No one can claim exclusive ownership
- Ensures the project remains open source
- Protects all contributors equally
- Maintains free usage for everyone
- Cannot be published elsewhere as proprietary - Derivative works must maintain the Apache 2.0 License
-
Fork the repository on GitHub
- Navigate to https://github.com/pasinduog/api-response
- Click the "Fork" button in the top-right corner
-
Clone your fork locally
git clone https://github.com/YOUR_USERNAME/api-response.git cd api-response -
Create a feature branch
git checkout -b feature/amazing-feature
-
Make your changes
- Follow the existing code style and conventions
- Add JavaDoc comments for all public methods and classes
- Ensure all existing tests pass (when tests are added)
- Keep changes focused and atomic
-
Commit your changes (follow conventional commit format)
git commit -m 'feat: add amazing feature' -
Push to your fork
git push origin feature/amazing-feature
-
Open a Pull Request on the main repository
- Go to the original repository on GitHub
- Click "New Pull Request"
- Select your branch and describe your changes in detail
- Reference any related issues
- β Follow existing code style and conventions
- β Add comprehensive JavaDoc comments for all public methods and classes
- β Include explicit constructor documentation for all constructors
- β Ensure all existing tests pass
- β Keep changes focused and atomic
- β Update README.md if adding new features or changing behavior
- β
Verify zero Javadoc warnings (
mvn javadoc:javadocshould run cleanly)
Follow Conventional Commits specification:
# Format: <type>(<scope>): <subject>
# Types:
feat: add new response wrapper method
fix: correct trace ID generation
docs: update installation instructions
refactor: improve exception handling
test: add unit tests for ApiResponse
style: format code and fix whitespace
chore: update dependencies
perf: optimize response creationExamples:
feat(dto): add pagination support to ApiResponse
fix(exception): resolve NPE in GlobalExceptionHandler
docs(readme): add contributing guidelines
test(response): add unit tests for success factory methods- Update documentation - Modify README.md if adding features or changing behavior
- Add JavaDoc - Document all new public methods and classes with zero warnings
- Write clear PR description:
- What problem does this solve?
- What changes were made?
- How was it tested?
- Any breaking changes?
- Reference issues - Link related issues using
#issue-numberorFixes #issue-number - Wait for review - Maintainer will review and provide feedback
- Address feedback - Make requested changes and push updates
- Squash commits - Clean up commit history if needed before merge
# Clone the repository
git clone https://github.com/pasinduog/api-response.git
cd api-response
# Build the project
mvn clean install
# Generate JavaDoc (builds with zero warnings)
mvn javadoc:javadoc
# Generate JavaDoc JAR (clean build with comprehensive documentation)
mvn javadoc:jar
# Package for Maven Central (requires GPG key)
mvn clean deploy -P releaseNote: The project includes explicit constructor documentation for all classes, ensuring zero Javadoc warnings during the build process. All constructors (including Spring bean constructors) are properly documented.
api-response/
βββ src/
β βββ main/
β β βββ java/io/github/pasinduog/
β β β βββ config/ # Auto-configuration classes
β β β βββ dto/ # Response wrapper classes
β β β βββ exception/ # Exception handling
β β βββ resources/
β β βββ META-INF/spring/ # Auto-configuration metadata
β βββ test/java/ # Unit tests (to be added)
βββ pom.xml # Maven configuration
βββ README.md # Documentation
- π Bug fixes
- π Documentation improvements
- β¨ New features (discuss in issue first)
- π§ͺ Test coverage improvements
- π¨ Code quality enhancements
- π Internationalization support
- Be respectful and inclusive
- Provide constructive feedback
This project is licensed under the Apache License 2.0 - see the LICENSE file for full details.
The Apache 2.0 License is a permissive open-source license that:
- β Use this library in personal, commercial, or proprietary projects
- β Modify and distribute the source code
- β Sublicense the code
- β Use the software for any purpose (private, commercial, etc.)
- β Patent protection - contributors grant you patent rights
- π‘οΈ No one can claim ownership - Others cannot copyright this work as their own
- π‘οΈ Attribution required - Anyone using or modifying this code must give credit
- π‘οΈ License propagation - Modified versions must include the Apache 2.0 License
- π‘οΈ Trademark protection - Project name and trademarks remain protected
- π‘οΈ No liability - Software is provided "as-is" without warranty
- π€ Open contribution - Anyone can contribute under the same terms
- π€ Grant of rights - By contributing, you grant Apache 2.0 rights to your contributions
- π€ Your work is protected - Contributions are attributed and cannot be claimed by others
- π€ Patent peace - Contributors cannot sue users for patent infringement related to their contributions
Copyright 2026 Pasindu OG
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
β You CAN:
- Use in commercial products
- Modify the code
- Distribute copies
- Grant sublicenses
- Use privately
β You CANNOT:
- Hold the author liable
- Use contributor names for endorsement
- Remove copyright notices
- Claim ownership of the original work
π You MUST:
- Include the Apache 2.0 License
- Include copyright notice
- State significant changes made
- Include the NOTICE file (if applicable)
For the complete license text, see LICENSE or visit Apache.org.
Pasindu OG
- π§ Email: pasinduogdev@gmail.com
- π GitHub: @pasinduog
- π» Repository: github.com/pasinduog/api-response
- Spring Boot team for the excellent framework
- The open-source community for inspiration and support
β Now Available on Maven Central!
This is a MAJOR version release with breaking changes. Please review carefully before upgrading from v2.0.0.
-
β Removed Lombok Dependency - BREAKING CHANGE
- v2.0.0 used Lombok with
@Builderannotation - v3.0.0 uses pure Java with manual builder pattern
- Impact: If your code depends on Lombok-generated methods, you must update
- Migration: Use the new
ApiResponse.ApiResponseBuilder<T>()constructor
- v2.0.0 used Lombok with
-
β Removed
traceIdField from ApiResponse - BREAKING CHANGE- v2.0.0 included
traceIdin success responses (ApiResponse) - v3.0.0 removes
traceIdfrom success responses entirely - Impact: Success response JSON no longer includes
traceIdfield - Migration: Trace IDs are now only in error responses (ProblemDetail)
- Use MDC or response headers for trace ID propagation instead
- v2.0.0 included
-
β Updated RFC Standard - Non-Breaking
- Upgraded from RFC 7807 to RFC 9457 (latest ProblemDetail standard)
- Same structure, just updated specification
- Enhanced Exception Handling - Added 6 new exception handlers for comprehensive error coverage (total: 10 handlers)
HttpMessageNotReadableException- Malformed JSON body handling (HTTP 400)MissingServletRequestParameterException- Missing required parameters (HTTP 400)NoResourceFoundException- 404 Not Found for endpoints/resources (HTTP 404)HttpRequestMethodNotSupportedException- Invalid HTTP method (HTTP 405)HttpMediaTypeNotSupportedException- Unsupported Content-Type (HTTP 415)NullPointerException- Null pointer handling with detailed stack traces (HTTP 500)
- TraceIdFilter - Automatic trace ID generation and MDC integration for distributed tracing
- RFC 9457 Compliance - Updated to latest RFC 9457 (supersedes RFC 7807) for ProblemDetail format
- Production-Ready Error Responses - Clear, actionable error messages for all common scenarios
- Zero External Dependencies - Removed Lombok dependency, now pure Java implementation
- Custom builder pattern implementation (no annotation processing required)
- Simpler setup - no IDE plugins needed
- Faster compilation - no annotation processing overhead
- Complete Javadoc Coverage - Zero warnings with comprehensive documentation
- Added package-info.java for all 5 packages
- Full @param, @return, @throws documentation
- Constructor documentation for all classes
- Enhanced class and method descriptions
- Apache 2.0 License - Comprehensive license documentation with contributor protections
- Maven Javadoc Plugin Configuration - Optimized settings for zero-warning builds
- Major version bump to 3.0.0 due to breaking changes
- Exception handling and auto-configuration features from v2.0.0 and v1.3.0 maintained
π§ Improvements:
- Stabilized API for long-term support (LTS) with semantic versioning
- Enhanced Documentation Quality:
- Zero Javadoc warnings during build process
- Complete API documentation with all @param, @return, @throws tags
- Explicit constructor documentation for all classes
- Package-level documentation for better organization
- Better Error Messages with specific, actionable details for all 10 exception types
- Improved Logging with consistent trace IDs across all exception handlers
- Performance Optimizations:
- Immutable response objects for thread safety
- Efficient builder pattern implementation
- Minimal memory footprint (~200 bytes per response)
- Fast response time (<1ms overhead)
- Build Quality Improvements:
- Clean Maven builds with zero warnings
- Optimized javadoc plugin configuration
- Production-ready artifact generation
- Enhanced CI/CD compatibility
π Documentation:
- Complete Javadoc Coverage - 100% documentation across all classes with zero warnings
ApiResponse- Full documentation including builder pattern with @param and @return tagsApiException- Abstract base class with comprehensive documentationGlobalExceptionHandler- All 10 exception handlers fully documentedTraceIdFilter- Filter implementation with @throws documentationApiResponseAutoConfiguration- Auto-configuration with detailed setup guide
- Package Documentation - Added package-info.java files for all 5 packages:
io.github.pasinduog- Root package overviewio.github.pasinduog.config- Configuration classes documentationio.github.pasinduog.dto- Data transfer objects documentationio.github.pasinduog.exception- Exception handling documentationio.github.pasinduog.filter- Servlet filters documentation
- Enhanced JavaDoc Quality:
- Added explicit constructor documentation for all classes
- Added comprehensive @param, @return, @throws tags
- Added @author, @version, @since tags to all classes
- Field-level documentation for all public/protected fields
- Detailed class-level descriptions with usage examples
- Updated all code examples and guides
- Added documentation for all 6 new exception handlers
π§ Technical Updates:
- Maven Javadoc Plugin - Optimized configuration for zero-warning builds
- Configured
doclint:nonefor flexible documentation standards - Added
failOnError:falseandfailOnWarnings:falsefor robust builds - Set
detectJavaApiLink:falseto avoid external API link issues - Optimized for Java 17+ with source level configuration
- Configured
- Removed Lombok Dependency - Pure Java implementation for better compatibility
- No IDE plugin requirements
- No annotation processing overhead
- Simpler build process
- Better IDE support out of the box
- Maintained full compatibility with Spring Boot 3.2.0 - 4.0.2
- Continued support for Java 17+ (Java 21 LTS recommended)
- All Maven plugins updated to latest stable versions
- Updated GlobalExceptionHandler with 10 comprehensive exception handlers
- Clean Build Process - Zero Javadoc warnings during
mvn javadoc:jar - Production-ready artifacts with complete documentation
This version included Lombok dependency and traceId in success responses, which have been removed in v3.0.0 for better compatibility and simpler implementation.
β Features (Now in v3.0.0 with improvements):
- Exception handling with ProblemDetail (RFC 7807)
- Auto-configuration support
- Trace ID support (with traceId in ApiResponse - removed in v3.0.0)
- Lombok-based builder pattern (changed to manual builder in v3.0.0)
Recommendation: Upgrade to v3.0.0 for pure Java implementation without Lombok dependency.
β New Features:
- Spring Boot Auto-Configuration - Added
ApiResponseAutoConfigurationwith automatic component registration - META-INF Auto-Configuration File - Included
org.springframework.boot.autoconfigure.AutoConfiguration.importsfor Spring Boot 3.x - Zero Manual Configuration - No more need for @ComponentScan or @Import annotations
- Type Mismatch Error Handler - Added MethodArgumentTypeMismatchException handling for better error messages
- Spring Boot 4.0.2 Support - Verified compatibility with the latest Spring Boot 4.x release
π§ Improvements:
- Updated Spring Boot version support to 4.0.2 for latest features and security
- Enhanced project stability and dependency management
- Improved JavaDoc documentation across all classes with comprehensive examples
- Added @since tags to all classes for better version tracking
- Refined build process and artifact generation
- Enhanced exception handling with more descriptive type conversion error messages
- Updated Maven plugins: maven-source-plugin (3.3.0), maven-javadoc-plugin (3.6.3), maven-gpg-plugin (3.1.0)
π Documentation:
- Added comprehensive auto-configuration documentation
- Updated FAQ section with auto-configuration details
- Enhanced all JavaDoc comments with detailed descriptions and examples
- Added migration notes for users upgrading from previous versions
- Added type mismatch error handling documentation
- Added performance benchmarks and characteristics
- Added JSON serialization behavior documentation
- Added Quick Links section for easy navigation
- Added Before/After comparison examples
- Added IDE setup instructions for contributors
π§ Technical Updates:
- Maintained compatibility with Java 17+ and Spring Boot 3.2.0 - 4.0.2
- Tested and verified full compatibility with Spring Boot 4.0.2
- Enhanced Maven Central publishing workflow with updated plugin versions
- Improved package structure and organization
- Updated build plugins: maven-source-plugin (3.3.0), maven-javadoc-plugin (3.6.3), maven-gpg-plugin (3.1.0)
β New Features:
- Custom ApiException Support - Abstract base class for creating domain-specific business exceptions
- Automatic ApiException Handling - GlobalExceptionHandler now catches and formats custom ApiException instances
- Response Status Field - Added
statusfield to ApiResponse for explicit HTTP status code in response body - Trace ID Support - GlobalExceptionHandler adds
traceId(UUID) to error responses for distributed tracing and log correlation - Improved Timestamp Format - Changed from
LocalDateTimetoInstant(UTC) for consistent timezone handling
π§ Improvements:
- Better support for microservices architecture with trace IDs in error responses
- Enhanced debugging capabilities with status codes in response body
- Cleaner exception handling pattern for business logic errors
- More consistent timestamp format across all responses
π Documentation:
- Added comprehensive examples for custom ApiException usage
- Updated all response examples to include new fields
- Enhanced best practices section
β New Features:
- Built-in
GlobalExceptionHandlerwith ProblemDetail (RFC 7807) support - Automatic validation error handling for
@Validannotations - Comprehensive exception logging with SLF4J
- Null pointer exception handling
- Standardized error response format with timestamps
π§ Improvements:
- Enhanced error responses with structured format
- Better integration with Spring Boot validation
- Automatic error field aggregation for validation failures
β Core Features:
- Standard API Response wrapper with generic type support
- Five static factory methods:
success(),created(),status() - Automatic ISO-8601 timestamp generation
- Full Spring Boot 3.2.0+ integration
- Immutable, thread-safe design
- Comprehensive JavaDoc documentation
π― Roadmap:
- Spring WebFlux support (reactive)
- Pagination metadata support
- OpenAPI schema generation
- Additional exception handlers
- Internationalization (i18n) support
- Response compression support
- Custom serialization options
- Metrics and monitoring integration
The API Response Library is a production-ready, zero-configuration solution for standardizing REST API responses in Spring Boot applications.
β
Instant Setup - Add dependency, start using. No configuration needed.
β
Battle-Tested - Used in production Spring Boot applications
β
Modern Standards - RFC 9457 ProblemDetail (v3.0.0+), Spring Boot 4.x support
β
Developer Friendly - Comprehensive docs, clear examples, active maintenance
β
Lightweight - Only ~10KB, zero runtime dependencies
β
Type Safe - Full generic support with compile-time checking
β
Pure Java - No Lombok or external dependencies (v3.0.0+)
| Metric | Value |
|---|---|
| JAR Size | ~10KB |
| Response Time | < 1ms |
| Memory per Response | ~200 bytes |
| Thread Safety | 100% |
| Spring Boot Support | 3.2.0 - 4.0.2 |
| Java Version | 17+ |
- π¦ Maven Central - Download & integration
- π JavaDoc - Complete API documentation
- π Issues - Report bugs or request features
- π¬ Discussions - Ask questions & share ideas
β If you find this library helpful, please give it a star on GitHub!
Made with β€οΈ by Pasindu OG