problem is a lightweight, opinionated Java library for treating failures as first-class problems.
Instead of relying solely on exceptions, it provides an immutable model to represent business, validation, and technical problems explicitly — while still preserving exception causes internally for diagnostics and observability, without leaking them outside the process.
In many Java applications, failures are either:
- represented only as exceptions, or
- flattened into strings or error codes at system boundaries.
This makes it difficult to:
- reason about failures as domain concepts
- compose errors functionally
- preserve context and intent
- safely retain technical causes for debugging
problem addresses this by modeling failures as explicit, immutable objects.
Failures are not exceptional — they are part of the domain.
A Problem (or Failure) represents something that went wrong, with:
- an explicit kind (business, validation, technical, etc.)
- a human-readable message
- optional error codes and i18n metadata
- optional contextual data
- optional nested problems (details)
- an optional Throwable cause, preserved internally
Problems are explicitly classified:
BUSINESS
VALIDATION
TECHNICAL
AUTHORIZATION
NOT_FOUND
CONFLICT
OTHERSThe kind expresses intent, not transport concerns (HTTP, messaging, etc.).
var problem = Failure.business("Insufficient funds")
.withCode("INSUFFICIENT_FUNDS")
.withData(Map.of(
"available", 50,
"requested", 100
));This represents a business problem, not an exception.
var problem = Failure.validation("Invalid request")
.withCode("VALIDATION_ERROR")
.withDetails(List.of(
Failure.validation("name is required")
.withCode("FIELD_REQUIRED")
.withData(Map.of("field", "name")),
Failure.validation("amount must be positive")
.withCode("INVALID_AMOUNT")
.withData(Map.of("field", "amount"))
));Validation errors are naturally modeled as a problem with nested problems.
try {
repository.save(entity);
} catch (SQLException ex) {
return Failure.technical("Database error")
.withCode("DB_ERROR")
.withCause(ex);
}- The original
Throwableis preserved - It is not serialized
- It is excluded from equals/hashCode
- It is not leaked in toString()
This allows full diagnostics inside the process while keeping error models safe.
Sometimes a business rule is enforced imperatively and throws an exception.
problem allows preserving the cause even for business problems:
try {
domainService.withdraw(account, amount);
} catch (IllegalStateException ex) {
return Failure.business("Withdrawal not allowed")
.withCode("WITHDRAWAL_NOT_ALLOWED")
.withCause(ex);
}The problem remains a business problem, but the technical cause is still available for logging and tracing.
problem works equally well with:
- functional styles (
Result,Either, railway-oriented programming) - imperative styles (exceptions as control flow)
It does not force a single error-handling paradigm.
Throwablecauses are never serialized- Collections are immutable snapshots
- The model is safe to expose via APIs using dedicated DTOs
This prevents accidental leakage of stack traces or internal details.
- Immutable by default
- Explicit over implicit
- Domain-driven
- Framework-agnostic
- Safe by construction
- ❌ A replacement for exceptions
- ❌ A logging framework
- ❌ An HTTP-only error model
It is a problem modeling library, not a transport or infrastructure abstraction.
- Java 17+
- No framework dependencies
This project is licensed under the Apache License. See the LICENSE file for more details.
This library is available on Maven Central. To use it, include the following dependency in your project's configuration file:
<dependency>
<groupId>codes.domix</groupId>
<artifactId>problem</artifactId>
<version>0.0.4</version>
</dependency>implementation("codes.domix:problem:0.0.4")