Introduction
Exception handling is easy to overlook when building an application.
A controller throws an exception. A global exception handler catches it. The application returns an HTTP status and an error message.
For a small application, that may be sufficient.
But when the application becomes part of a distributed system, exception handling becomes an API design problem.
Consider three Spring Boot services returning errors.
Customer Service:
{
"message": "Customer not found"
}
Payment Service:
{
"error": "PAYMENT_ALREADY_PROCESSED",
"status": 409
}
Inventory Service:
{
"timestamp": "2026-10-18T10:30:00Z",
"status": 404,
"error": "Not Found",
"path": "/inventory/123"
}
All three services report failures, but each exposes a different response structure.
Now imagine an API gateway, frontend application, or another microservice attempting to interpret these responses.
Every integration requires additional error-handling logic.
The problem is not necessarily that exceptions are handled incorrectly.
The problem is that exception handling has not been designed as a consistent API contract.
1. An Exception Is Not an API Error Contract
Java exceptions and API error responses serve different purposes.
An exception represents a failure inside an application. An API error response communicates that failure across an application boundary.
Consider:
throw new IllegalStateException(
"Customer account is disabled"
);
This exception is meaningful to the developer.
However, an API consumer needs more than a Java exception message.
It may need to understand:
- What failure occurred?
- Which application error code identifies it?
- What HTTP status represents the failure?
- Is the failure potentially retryable?
- Which service originated the error?
- What additional information is available for troubleshooting?
These concerns should not depend on Java exception class names or arbitrary exception messages.
An API error contract provides a stable representation independent of the internal exception hierarchy.
2. Designing the Error Contract
A consistent error contract should provide enough information for applications and operational systems to understand a failure.
In NERV Exception, the HTTP error response is represented
by NervErrorResponse.
The following example illustrates a business-related conflict
using the native CONFLICT error definition.
{
"code": "CONFLICT",
"message": "Duplicate resource",
"status": 409,
"retryable": false,
"category": "CLIENT",
"traceId": null,
"spanId": null,
"path": "/nerv",
"timestamp": "2026-10-18T00:00:00Z",
"origin": {
"service": "unknown-service",
"instance": "unknown-instance",
"version": "unknown-version",
"environment": "default"
},
"details": {}
}
This example reflects the structure supported by the NERV Exception implementation.
The timestamp is illustrative. The null tracing fields assume no active tracing context and null-inclusive JSON serialization. Applications may configure Jackson to omit null fields.
Notice that the native CONFLICT definition uses
the category CLIENT.
Although the example represents a business conflict, the category is determined by the error definition, not automatically inferred from the use case.
Let's examine the responsibilities of each field.
Application Error Code
The code field provides a stable,
machine-readable identifier.
"code": "CONFLICT"
Application consumers can use error codes to implement predictable behavior without parsing human-readable messages.
For example, an application might define its own domain-specific codes:
CUSTOMER_NOT_FOUND
CUSTOMER_DISABLED
PAYMENT_ALREADY_PROCESSED
These are illustrative application codes, not built-in NERV Exception definitions.
The important principle is that error codes should remain stable once external consumers depend on them.
Human-Readable Message
The message field explains the failure.
"message": "Duplicate resource"
Messages should be understandable, but they should not be treated as stable application identifiers.
Messages may evolve for clarity or localization.
They should also avoid exposing sensitive information, including internal stack traces, credentials, and database implementation details.
HTTP Status
The status field represents the HTTP status
associated with the selected error definition.
"status": 409
NERV Exception's Spring Web handler uses the mapped response status as the actual HTTP response status.
This allows the response body and HTTP transport to communicate the same selected application failure.
However, when errors are decoded from downstream services, the selected status may come from a local error definition rather than the original downstream transport status.
This distinction matters when designing error propagation.
Error Category
The category field classifies the nature
of the failure.
"category": "CLIENT"
NERV Exception supports categories through error definitions.
Its API declares several category constants, including VALIDATION, BUSINESS, SECURITY, DEPENDENCY, and SYSTEM.
However, the serialized category is a string.
The implementation also uses additional category values such as CLIENT, DOWNSTREAM, and DATA.
Therefore, applications should not assume that every error category belongs to one closed enumeration.
Importantly, the category does not automatically determine the HTTP status or retryability.
Those properties are independently defined by the selected error definition.
Retryability
The retryable field communicates whether
a failure is considered potentially retryable.
"retryable": false
This information becomes especially important when services communicate over unreliable networks.
For example, a validation failure generally should not be retried without correcting the request.
A temporary dependency failure may be eligible for retry.
But retryability is not determined solely by HTTP status.
In NERV Exception, retryability comes from the selected error definition.
For example, the native INTERNAL_SERVER_ERROR definition is non-retryable, while TOO_MANY_REQUESTS and OPTIMISTIC_LOCK_CONFLICT are retryable.
A retryable error is a signal for retry policy, not a guarantee that a retry will occur.
The caller must still consider idempotency, retry limits, backoff, and operation safety.
Tracing Information
The response includes:
{
"traceId": null,
"spanId": null
}
When Micrometer tracing is available and an active trace context exists, NERV Exception can obtain the corresponding trace and span identifiers.
Otherwise, those values may be null.
NERV Exception reads the existing tracing context. It does not automatically create a new tracing span for every exception.
Error Origin
The origin object identifies the service
associated with the failure.
{
"origin": {
"service": "customer-service",
"instance": "customer-service-01",
"version": "1.0.0",
"environment": "production"
}
}
This is an illustrative origin object.
In Spring Boot, NERV Exception can resolve origin information from application configuration and runtime information.
When an exception already contains an explicit origin, that origin takes precedence.
This is useful when a failure originates in a downstream service and is later exposed by another application.
However, origin is not a complete service-hop history.
For full distributed request tracing, applications should use an appropriate tracing system.
Structured Details
The details field contains structured
additional information.
For example:
{
"details": {
"requestedQuantity": 150,
"maximumQuantity": 100
}
}
This allows API consumers to process useful context without parsing exception messages.
NERV Exception uses details,
not a top-level field named metadata.
Applications should carefully control what information is included.
Structured details should not expose credentials, personal information, or internal implementation details that are inappropriate for the API consumer.
3. HTTP Status Codes Are Necessary but Not Sufficient
HTTP status codes describe the outcome of an HTTP request at the protocol level.
For example:
- 400 — Bad Request
- 401 — Unauthorized
- 403 — Forbidden
- 404 — Not Found
- 409 — Conflict
- 429 — Too Many Requests
- 500 — Internal Server Error
- 503 — Service Unavailable
However, HTTP status codes rarely describe the complete application-level failure.
Consider two errors:
PAYMENT_ALREADY_PROCESSED
ORDER_ALREADY_CANCELLED
Both could reasonably map to HTTP 409.
But they represent different business conditions.
A consumer may need to respond differently to each.
This leads to an important distinction:
HTTP status describes the protocol outcome. Application error codes describe the application failure.
A consistent error contract preserves both.
4. Consistency Across Microservices
Within a monolithic application, a global exception handler can provide a consistent error response.
In a distributed architecture, every service may act as both an API provider and an API consumer.
API Gateway
|
v
Order Service
|
+---- Customer Service
|
+---- Inventory Service
|
+---- Payment Service
Suppose Payment Service rejects an operation because the payment has already been processed.
The response travels back through Order Service and eventually reaches the API gateway.
Without a shared contract, the original application error may be replaced by a generic HTTP client exception.
That can cause several problems:
- Loss of meaningful application error codes
- Incorrect error classification
- Incorrect retry decisions
- Additional service-specific parsing logic
- Increased coupling between applications
A common error contract reduces these inconsistencies.
However, sharing a contract does not mean every service must share the same business error definitions.
5. Handling Unknown Downstream Error Codes
Consider a downstream service returning the following NERV-compatible error response:
{
"code": "REMOTE_FAILURE",
"message": "Downstream explanation",
"status": 422,
"retryable": false,
"category": "REMOTE_CATEGORY",
"origin": {
"service": "remote-service"
},
"details": {
"reason": "remote detail"
}
}
Suppose the calling service does not recognize REMOTE_FAILURE in its local error registry.
Should the original code be discarded?
Should the failure become a generic integration error?
Should the caller assume the operation is retryable?
NERV Exception provides a mechanism for preserving the semantics of unknown downstream error codes.
Its Feign decoder can create a
RemoteNervErrorCode
using the information supplied by the downstream response.
For a recognized NERV error envelope, the decoder requires a nonblank code, a nonnull message, and a body status between 400 and 599.
When the code is unknown locally, the remote definition retains the downstream code, message, body status, retryability, and category.
The resulting downstream exception also carries supported origin and details information.
This allows services to preserve meaningful failures without importing every downstream business error catalog.
Known Error Codes Behave Differently
There is an important distinction when the downstream error code is already registered locally.
In that case, NERV Exception uses the local error definition as the authority for:
- HTTP status
- Error category
- Retryability
The remote message, origin, and supported details can still be carried by the downstream exception.
This means propagation is not always a byte-for-byte preservation of the original response.
For example, the receiving service may generate a new timestamp, path, and tracing information when rendering its own HTTP response.
The original downstream traceId, spanId, path, and timestamp are not retained by the downstream exception.
This distinction is intentional to understand when designing service boundaries.
A shared error contract supports controlled propagation, not necessarily lossless forwarding.
What About Invalid Downstream Responses?
Not every downstream response follows the NERV contract.
A service may return malformed JSON, an HTML error page, or an unrelated error structure.
When NERV Exception cannot recognize the response as a valid NERV error envelope, its Feign decoder delegates to the configured fallback decoder.
The default fallback is Feign's standard error decoder.
Connection failures that occur before an HTTP response is received are outside the response decoder's responsibility.
These failures must be handled through the HTTP client's transport and resilience configuration.
6. Retryability Is Not the Same as Retry Execution
Retryability is often misunderstood in distributed systems.
A response containing:
{
"retryable": true
}
does not mean the operation will automatically be retried.
In NERV Exception, the Feign decoder can wrap a retryable
downstream error in a Feign RetryableException.
The underlying NervDownstreamException
is retained as the cause.
However, whether a retry actually occurs depends on the configured Feign retry policy.
NERV Exception does not install a retry policy through this decoder.
There is another important limitation.
If the retryable wrapper reaches the default generic Spring Web exception handler, it may be mapped to an internal server error rather than being unwrapped into the original downstream NERV error.
Therefore, applications should not assume that retryable downstream errors will always be transparently propagated after retries are exhausted.
Reliable retry handling requires coordination between:
- Error classification
- HTTP client retry configuration
- Operation idempotency
- Backoff and retry limits
- Exception handling after retry exhaustion
The error contract provides information. The application architecture determines how to use it.
7. Validation Errors Need Structured Information
Validation failures are another reason to use a consistent error contract.
Consider a request containing invalid input.
Returning only:
{
"message": "Validation failed"
}
forces the consumer to guess which fields are invalid.
NERV Exception can expose field validation errors
through the details.errors structure.
For example:
{
"code": "VALIDATION_ERROR",
"message": "Validation error",
"status": 400,
"retryable": false,
"category": "VALIDATION",
"details": {
"errors": {
"name": "Name is required"
}
}
}
This is an abbreviated example of the supported validation response structure.
The complete response can also include tracing, path, timestamp, and origin information.
The field errors are represented as an object mapping field names to messages, not as an array.
The mapper retains the first message for each field key.
This allows frontend applications to associate validation messages with specific form fields.
The availability of details depends on the configured exception-handling settings.
8. Designing Error Contracts for Evolution
An error contract is part of an application's public interface.
Once clients depend on specific error codes and semantics, changes must be introduced carefully.
Keep Error Codes Stable
Avoid renaming published error codes without a compatibility strategy.
Separate Codes from Messages
Messages may change for clarity or localization. Application behavior should depend on stable error codes.
Define Unknown Error Behavior
Clients should be prepared to encounter error codes introduced after they were deployed.
An unknown error code should not automatically cause the entire error response to become unusable.
Use Additive Changes Carefully
New optional fields can make an error contract more useful, but consumers must be configured to tolerate them.
NERV Exception does not automatically enforce a complete error schema versioning strategy.
Compatibility policies remain an architectural responsibility.
Preserve Semantic Meaning
Changing an error from non-retryable to retryable can affect application behavior.
Changing its HTTP status or category can also affect clients and operational systems.
These changes should be treated as behavioral API contract changes.
9. Implementing the Contract with NERV Exception
The principles described in this article can be implemented using Spring's exception-handling infrastructure.
However, maintaining the same infrastructure independently across multiple services introduces duplicated code and inconsistent behavior.
NERV Exception provides reusable components for standardizing error handling in Java and Spring Boot.
Its modules include:
- nerv-exception-api
- nerv-exception-core
- nerv-exception-spring-web
- nerv-exception-spring-feign
- nerv-exception-spring-boot-starter
- nerv-exception-jpa
- nerv-exception-event
- nerv-exception-spring-kafka
The library separates shared error definitions, exception handling, HTTP integration, and transport-specific functionality.
Applications can define their own error codes
through the NervErrorCode interface.
The interface exposes the application error code, message, and HTTP status, with optional definitions for additional semantics.
The default category is GENERAL, and the default retryability is false.
Applications may implement these definitions using Java enums, although enums are not mandatory.
The library also provides error code registries for resolving known codes.
A composite registry can combine multiple registries, with resolution using the first matching definition.
This is useful for applications that maintain multiple groups of error definitions.
However, duplicate detection is not globally enforced across all composite registry delegates.
Applications should establish clear ownership and naming conventions for their error codes.
Spring Boot Integration
The Spring Boot starter provides auto-configuration for supported exception-handling components.
Spring Web integration supports common MVC failures, including validation errors, unreadable request bodies, missing parameters, and unexpected exceptions.
The implementation also supports application-defined NERV exceptions.
However, not every exception occurring in a Spring Boot application passes through MVC exception advice.
For example, authentication failures occurring inside Spring Security filters require appropriate handling at the security boundary.
Applications should not assume that every authentication or authorization failure automatically receives the same NERV error response.
Production Security Configuration
Error contracts should provide useful information without exposing internal implementation details.
This is particularly important for unexpected exceptions.
In the reviewed NERV Exception 1.4.0 implementation, the Spring Boot starter property defaults enable internal message exposure and exception causes.
The standalone default settings object uses more restrictive values.
For public APIs, explicitly configure the application to avoid exposing internal exception messages and causes.
Stack traces should also remain disabled in public responses.
The exact configuration property names should be taken from the version of NERV Exception used by the application.
Also remember that disabling general details alone does not necessarily disable separately configured cause or stack-trace exposure.
A consistent error contract should make failures easier to understand without making the application easier to exploit.
10. A Shared Contract Is Not a Shared Business Model
There is an important architectural distinction between sharing an error contract and sharing business error definitions.
Consider three independent services.
Customer Service
CUSTOMER_NOT_FOUND
CUSTOMER_DISABLED
Payment Service
PAYMENT_ALREADY_PROCESSED
PAYMENT_DECLINED
Inventory Service
INVENTORY_NOT_AVAILABLE
INVENTORY_RESERVATION_EXPIRED
Each service owns its business error codes.
However, all three can expose the same structural contract.
Application Error
|
+-- Code
|
+-- Message
|
+-- Status
|
+-- Category
|
+-- Retryability
|
+-- Trace Information
|
+-- Origin
|
+-- Details
This provides consistency without requiring every service to import every other service's business error definitions.
It also allows services to evolve independently while preserving a common language for failures.
Share the structure of errors. Keep ownership of business errors within the services that define them.
11. Common Mistakes When Designing Error Contracts
Returning Every Failure as HTTP 500
A validation error or business conflict should not automatically become an internal server error.
Using Exception Messages as Error Identifiers
Messages can change. Stable error codes are more appropriate for machine-readable application behavior.
Assuming Error Categories Determine HTTP Status
Category, status, and retryability are separate concerns.
Applications should not assume that a particular category always maps to one HTTP status.
Assuming Every Integration Error Is Retryable
A downstream service may reject a request because of a permanent business condition.
Retrying without understanding the failure can create unnecessary load and unexpected behavior.
Assuming Retryability Guarantees Retries
Retryability describes an error property. Actual retries depend on the client and its configured policy.
Discarding Unknown Downstream Codes
A service should not need to recognize every downstream business error to preserve a valid error contract.
Exposing Internal Exception Details
Structured error responses should not expose credentials, stack traces, or sensitive implementation details to unauthorized consumers.
Assuming Error Propagation Is Lossless
A receiving service may resolve an error using a local definition and generate its own request path, timestamp, and tracing information.
Applications should understand these boundaries rather than assume every downstream field is preserved unchanged.
12. Final Thoughts
Consistent exception handling is not simply about returning the same JSON structure from every controller.
It is about establishing a reliable language for communicating failures between systems.
A well-designed error contract separates several responsibilities:
- HTTP status communicates the protocol outcome.
- Error codes identify application failures.
- Messages explain failures to humans.
- Categories classify the nature of failures.
- Retryability informs recovery decisions.
- Tracing connects failures to request execution.
- Origin identifies the service associated with the failure.
- Details provide structured additional context.
These responsibilities should remain distinct, even when they are represented in a single response.
NERV Exception provides one implementation of this approach, with reusable components for Spring Boot exception handling, error code resolution, and controlled downstream propagation.
The broader architectural lesson is independent of the library itself.
A good error contract does not merely report that something failed. It helps the receiving system understand the failure and make an informed decision about what to do next.
That is the foundation of reliable error handling in distributed applications.
About NERV Exception
NERV Exception is part of the NERV ecosystem (Next-Generation Engineering for Runtime Velocity), a collection of reusable Java and Spring Boot libraries focused on production-oriented application architecture.
Explore the open-source projects: CzetsuyaTech on GitHub .

Post a Comment