Definition
- Result object holds either a success value or an error (Either pattern).
- Mapping step runs the risky operation in try-catch, returning success or failure.
- partitioningBy(isSuccess) splits into two groups: successes and failures.
- Partial success: every element is processed; bad ones do not abort the pipeline.
- Use case: batch processing where some records are malformed but the rest should proceed.
At National Bank of India, Priya processes each transaction form. Instead of stopping at the first bad form, she stamps every form as either 'done' or 'failed', then sorts them into a done pile and a failed pile. The good ones proceed, and the bad ones go to the error report.
Understanding the Concepts
Why not just throw?
In a stream pipeline, throwing on the first bad element aborts the entire pipeline. For batch processing with thousands of records, this means losing all progress because of one malformed entry. The result-object approach processes every element and classifies each as success or failure.
How do you implement it?
package com.bank.service;
import java.math.BigDecimal;
import java.util.*;
import java.util.stream.Collectors;
public class CollectSuccessesAndErrors {
record Result<T>(T value, String error) {
boolean isSuccess() { return error == null; }
static <T> Result<T> success(T value) { return new Result<>(value, null); }
static <T> Result<T> failure(String error) { return new Result<>(null, error); }
}
static Result<BigDecimal> parseAmount(String raw) {
try {
return Result.success(new BigDecimal(raw));
} catch (NumberFormatException e) {
return Result.failure("Invalid amount: " + raw);
}
}
public static void main(String[] args) {
List<String> rawAmounts = List.of(
"1500000", "invalid", "900000", "abc", "1200000");
Map<Boolean, List<Result<BigDecimal>>> partitioned = rawAmounts.stream()
.map(CollectSuccessesAndErrors::parseAmount)
.collect(Collectors.partitioningBy(Result::isSuccess));
List<BigDecimal> successes = partitioned.get(true).stream()
.map(Result::value)
.toList();
List<String> errors = partitioned.get(false).stream()
.map(Result::error)
.toList();
System.out.println("Successes: " + successes);
System.out.println("Errors: " + errors);
System.out.printf("Processed %d of %d successfully%n",
successes.size(), rawAmounts.size());
}
}
Successes: [1500000, 900000, 1200000]
Errors: [Invalid amount: invalid, Invalid amount: abc]
Processed 3 of 5 successfully
How does the Result type work?
| Factory | Value | Error | isSuccess |
|---|---|---|---|
| Result.success(value) | Present | null | true |
| Result.failure(error) | null | Present | false |
What about using Optional instead of a custom Result?
// Simpler but loses the error message
List<Optional<BigDecimal>> results = rawAmounts.stream()
.map(s -> {
try { return Optional.of(new BigDecimal(s)); }
catch (NumberFormatException e) { return Optional.<BigDecimal>empty(); }
})
.toList();
// Keep only successes
List<BigDecimal> values = results.stream()
.flatMap(Optional::stream)
.toList();
// Downside: no information about which inputs failed or why
How is the Either/Result pattern different from throwing?
| Aspect | Throwing | Result/Either |
|---|---|---|
| First error | Aborts pipeline | Continues processing |
| Error info | Exception message + stack trace | Stored in result object |
| Partial results | Lost | Preserved |
| Use case | Exceptional, unrecoverable | Expected, per-element |
What are common mistakes?
Mistake: Using exceptions for expected per-element failures in batch processing.
// WRONG: first bad record aborts the entire batch
rawAmounts.stream()
.map(BigDecimal::new) // throws NumberFormatException on "invalid"
.toList(); // aborts, all previous results lost
// CORRECT: result-object approach processes all records
rawAmounts.stream()
.map(CollectSuccessesAndErrors::parseAmount)
.collect(Collectors.partitioningBy(Result::isSuccess));
What about using vavr's Either or Try?
Libraries like vavr provide production-grade Either and Try types with rich APIs. For production code, consider using them instead of a hand-rolled Result.
What should a senior engineer know?
This is the functional alternative to exceptions for expected failures. It keeps the pipeline total (every input produces an output) and lets you aggregate errors. The decision between result-objects and exceptions is about whether partial success matters for the task.
What are common interview follow-up questions?
- "When should you throw vs use Result?" Throw for unrecoverable errors; use Result for expected per-element failures.
- "How do you retry failed records?" Collect the failure inputs and feed them through a retry pipeline.
- "What library provides Either?" vavr (previously javaslang).
Visual Overview
Input: ["1500000", "invalid", "900000", "abc", "1200000"]
│
│ map(parseAmount)
▼
[Success(1.5M), Failure(invalid), Success(0.9M),
Failure(abc), Success(1.2M)]
│
│ partitioningBy(isSuccess)
▼
┌──────────────────────────────────────┐
│ true → [1.5M, 0.9M, 1.2M] │ ← successes
│ false → [invalid, abc] │ ← errors
└──────────────────────────────────────┘
Key Takeaway
To collect both successes and errors, stop throwing and model each element's outcome as a result object holding either a success value or an error. The mapping step runs the risky operation in try-catch and returns a success or failure result. Then partitioningBy(isSuccess) splits the stream into the two groups.
At the National Bank of India, Priya uses this for a batch that parses transaction amounts where some records are malformed. The job processes all valid records and collects the invalid ones for an error report, instead of failing entirely on the first bad record.
This is the functional alternative to exceptions for expected per-element failures. It keeps the pipeline total (every input produces an output) and lets you aggregate errors rather than losing all progress. For truly exceptional, unrecoverable errors, still throw.
Remember one sentence: map each element to a success-or-failure result object and partition, so one bad element does not abort the whole batch.
---