尚未检测到更新。GitHub 发布版本后会显示在这里。
制作者的介绍仅有上面的一句话。 更多详情请阅读仓库 README。 仓库 README ↗
正在加载完整 README。
查看完整 README ↗A bridge between deterministic and non-deterministic worlds.
Most application code is deterministic. Given the same inputs, it produces the same outputs. But applications routinely encounter non-determinism — operations whose outcomes depend on factors outside the code's control: network availability, service responsiveness, database state, or even intentional randomness in stochastic algorithms.
Not all non-determinism implies failure. A random number generator is non-deterministic yet doesn't fail. However, in practice, the vast majority of non-deterministic operations Java developers encounter are fallible: network calls, database queries, external API invocations. These may timeout, return errors, or simply not respond. Outcome addresses this dominant case.
It provides a formal boundary where deterministic code meets fallible operations. At this boundary, uncertainty is acknowledged, observed, and converted into one of two well-defined states: Ok or Fail. From there, application code flows deterministically again — handling success or failure through normal control flow, not exception handling.
Java's exception model conflates three fundamentally different things:
Operational failures — network timeouts, service unavailability, rate limits. These are normal. They happen every day in production. They're expected, recoverable, and often transient.
Defects — null pointers, invalid arguments, misconfiguration. These are bugs. No retry will help. A human must fix the code or the configuration.
Terminal environment failures — OutOfMemoryError, StackOverflowError, NoClassDefFoundError. The JVM itself is compromised. No application-level handling is possible or advisable.
By treating the first two as "exceptions," Java's type system forces a syntactic ritual (try/catch) that looks like handling but usually isn't. Developers comply with the compiler, not with operational reality:
try {
return httpClient.send(request);
} catch (IOException e) {
log.error("Request failed", e); // Now what?
throw new RuntimeException(e); // Punt.
}
The result:
Operational failures are values. They belong in normal control flow, not exception handling.
Outcome<Response> result = boundary.call("UserApi.fetch", () -> httpClient.send(request));
return switch (result) {
case Outcome.Ok(var response) -> processResponse(response);
case Outcome.Fail(var failure) -> handleFailure(failure);
};
Defects remain exceptions. A NullPointerException should crash the operation and page an operator. No catch block will fix a bug.
Terminal errors are not handled. An OutOfMemoryError means the JVM is compromised. The framework does not catch Error—let the process die and let your infrastructure (Kubernetes, systemd, supervisors) restart it.
This isn't a new idea—it's how Rust, Go, and functional languages handle errors. Outcome brings this proven idiom to Java with full type safety and pattern matching support.
A sealed interface representing success or failure:
public sealed interface Outcome<T> permits Outcome.Ok, Outcome.Fail {
record Ok<T>(T value) implements Outcome<T> {}
record Fail<T>(Failure failure) implements Outcome<T> {}
}
Use pattern matching for exhaustive handling:
String message = switch (outcome) {
case Outcome.Ok(var user) -> "Hello, " + user.name();
case Outcome.Fail(var failure) -> "Error: " + failure.message();
};
Or use combinators for functional composition:
Outcome<Order> order = fetchUser(userId)
.flatMap(user -> fetchCart(user.cartId()))
.flatMap(cart -> createOrder(cart));
A structured failure with everything needed for reporting and policy decisions:
network:timeout, sql:connection)TRANSIENT, PERMANENT, or DEFECTRetry-After header)This isn't heavyweight ceremony—it's the information operators need to respond appropriately.
The single point where checked exceptions are translated into outcomes:
Boundary boundary = new Boundary(classifier, reporter);
Outcome<Response> result = boundary.call("HttpClient.send", () ->
httpClient.send(request) // throws IOException
);
Outcome.Fail (reported to operations)OperationalExceptionHandler at the top)The Boundary ensures checked exceptions never leak into application code while maintaining full observability.
Failures are first-class concerns, not afterthoughts buried in log files:
public interface OpReporter {
void report(Failure failure);
void reportRetryAttempt(Failure failure, int attemptNumber, Duration delay);
void reportRetryExhausted(Failure failure, int totalAttempts);
}
OpReporter implementations decide how to handle each failure based on its FailureType and their own configuration. Route to your observability stack—metrics, structured logging, Slack, Teams, or whatever your organization uses.
Retry logic belongs in policies, not scattered catch blocks:
Retrier retrier = Retrier.builder()
.policy(RetryPolicy.backoff(5, Duration.ofMillis(100), Duration.ofSeconds(10)))
.reporter(reporter)
.build();
Outcome<Response> result = retrier.execute(
() -> boundary.call("UserApi.fetch", () -> userApi.fetch(userId))
);
以上为 README 的开头部分。其余内容请阅读原文。 查看完整 README ↗