Chapter 20: Troubleshooting & FAQ
This chapter consolidates common issues, debugging techniques, and frequently asked questions about JBCT.
Debugging Monadic Chains
Problem: âI canât see whatâs happening inside flatMapâ
Symptom: You have a chain like this and canât figure out where it fails:
return validateRequest(request)
.flatMap(this::checkPermissions)
.flatMap(this::processData)
.flatMap(this::saveResult);
Solution 1: Add intermediate logging
return validateRequest(request)
.onSuccess(r -> log.debug("Validated: {}", r))
.onFailure(c -> log.debug("Validation failed: {}", c.message()))
.flatMap(this::checkPermissions)
.onSuccess(r -> log.debug("Permissions OK: {}", r))
.onFailure(c -> log.debug("Permission denied: {}", c.message()))
.flatMap(this::processData)
// ...
Solution 2: Use IDE breakpoints
Set breakpoints inside the lambda:
.flatMap(data -> {
return processData(data); // Breakpoint on this line
})
Solution 3: Extract to named methods
return validateRequest(request)
.flatMap(this::checkPermissionsStep) // Breakpoint in method
.flatMap(this::processDataStep)
.flatMap(this::saveResultStep);
private Result<Permissions> checkPermissionsStep(ValidRequest request) {
// Breakpoint here, full visibility
return checkPermissions(request);
}
Problem: âMy Promise never resolvesâ
Symptom: Test hangs on .await() or production code times out.
Checklist:
- Is the Promise created but never resolved?
Promise<User> promise = Promise.promise(); // Unresolved!
// Missing: promise.succeed(user) or promise.fail(cause)
- Is there a deadlock in async code?
// WRONG: Blocking inside async chain
.flatMap(data -> {
return otherPromise.await().async(); // Blocks the thread!
})
// CORRECT: Chain without blocking
.flatMap(data -> otherPromise)
- Is Promise.lift wrapping blocking code?
// This runs async - make sure the thread pool isn't exhausted
Promise.lift(() -> {
Thread.sleep(10000); // Blocks a thread
return result;
});
Debug technique: Add timeout to isolate:
promise
.timeout(TimeSpan.timeSpan(5).seconds())
.await()
.onFailure(cause -> log.error("Timed out or failed: {}", cause));
Problem: âIâm getting CompositeCause but donât know which validations failedâ
Symptom: Result.all() fails but you only see âMultiple failuresâ.
Solution: Inspect the CompositeCause:
result.onFailure(cause -> {
if (cause instanceof CompositeCause composite) {
composite.causes().forEach(c ->
log.error("Validation failure: {}", c.message())
);
} else {
log.error("Single failure: {}", cause.message());
}
});
Better solution: Create specific error messages in value objects:
public record Email(String value) {
private static final Fn1<Cause, String> INVALID =
Causes.forOneValue("Invalid email format: %s");
public static Result<Email> email(String raw) {
return Verify.ensure(raw, Verify.Is::notBlank)
.filter(INVALID, PATTERN.asMatchPredicate())
.map(Email::new);
}
}
Now CompositeCause contains âInvalid email format: xyzâ instead of generic message.
Common Mistakes and Fixes
Mistake: Nested Result/Promise
// WRONG: Returns Result<Result<User>>
public Result<Result<User>> findUser(String id) {
return UserId.userId(id)
.map(this::lookupUser); // lookupUser returns Result<User>
}
Fix: Use flatMap instead of map:
// CORRECT: Returns Result<User>
public Result<User> findUser(String id) {
return UserId.userId(id)
.flatMap(this::lookupUser);
}
Mistake: Ignoring the Result
// WRONG: Result is created but never used
public void processOrder(Order order) {
validateOrder(order); // Returns Result<ValidOrder> but ignored!
saveOrder(order);
}
Fix: Always handle the Result:
// CORRECT: Result flows through
public Result<OrderId> processOrder(Order order) {
return validateOrder(order)
.flatMap(this::saveOrder);
}
Mistake: Throwing inside monadic chain
// WRONG: Exception breaks the chain
.flatMap(data -> {
if (data.isEmpty()) {
throw new IllegalStateException("Empty data"); // Escapes!
}
return process(data);
})
Fix: Return failure instead:
// CORRECT: Error stays in the chain
.flatMap(data -> {
if (data.isEmpty()) {
return EMPTY_DATA_ERROR.result();
}
return process(data);
})
// BETTER: Use filter
.filter(EMPTY_DATA_ERROR, data -> !data.isEmpty())
.flatMap(this::process)
Mistake: Multi-statement lambda
// WRONG: Violates single level of abstraction
.map(user -> {
log.info("Processing user: {}", user.id());
String normalized = user.email().toLowerCase();
cache.put(user.id(), user);
return new ProcessedUser(user.id(), normalized);
})
Fix: Extract to named method:
// CORRECT: Simple lambda, logic in method
.map(this::processUser)
private ProcessedUser processUser(User user) {
log.info("Processing user: {}", user.id());
String normalized = user.email().toLowerCase();
cache.put(user.id(), user);
return new ProcessedUser(user.id(), normalized);
}
Mistake: Using Optional instead of Option
// WRONG: Mixing Java Optional with Pragmatica types
public Result<User> findUser(UserId id) {
Optional<User> optional = repository.findById(id);
if (optional.isPresent()) {
return Result.success(optional.get());
}
return USER_NOT_FOUND.result();
}
Fix: Convert at the boundary:
// CORRECT: Wrap immediately
public Result<User> findUser(UserId id) {
return Option.option(repository.findById(id).orElse(null))
.toResult(USER_NOT_FOUND);
}
// OR: Use Option.from()
public Result<User> findUser(UserId id) {
return Option.from(repository.findById(id))
.toResult(USER_NOT_FOUND);
}
Mistake: Bypassing factory method
// WRONG: Direct constructor call
Email email = new Email("[email protected]"); // Skips validation!
// CORRECT: Use factory method
Result<Email> email = Email.email("[email protected]");
IDE Setup
IntelliJ IDEA
Recommended settings:
-
Enable parameter hints for lambdas:
- Settings â Editor â Inlay Hints â Java â Parameter hints
- Enable âShow hints for lambdasâ
-
Fold anonymous classes:
- Settings â Editor â General â Code Folding
- Enable âLambda expressionsâ
-
Live templates for JBCT patterns:
// Template: jbctvo (Value Object)
public record $NAME$($TYPE$ value) {
private static final Cause INVALID = Causes.cause("Invalid $NAME{{CONTENT}}quot;);
public static Result<$NAME{{CONTENT}}gt; $FACTORY$(String raw) {
return Verify.ensure(raw, Verify.Is::notBlank)
.map($NAME$::new);
}
}
// Template: jbctuc (Use Case)
public interface $NAME$ {
Promise<Response> execute(Request request);
record Request($PARAMS$) {}
record Response($RESULT$) {}
static $NAME$ $FACTORY$($DEPS$) {
return request -> ValidRequest.validRequest(request)
.async()
.flatMap($STEPS$);
}
}
VS Code
Recommended extensions:
- Extension Pack for Java
- Java Test Runner
settings.json:
{
"java.completion.chain.enabled": true,
"java.completion.filteredTypes": [
"java.util.Optional" // Suggest Option instead
]
}
Testing Troubleshooting
Problem: âTest passes but shouldnâtâ
Symptom: You expect failure but test passes silently.
@Test
void shouldFail() {
Email.email("invalid"); // Returns Result, but test doesn't check it!
}
Fix: Always assert:
@Test
void email_fails_forInvalidInput() {
Email.email("invalid")
.onSuccess(Assertions::fail); // Fail if unexpectedly succeeds
}
Problem: âAsync test is flakyâ
Symptom: Test sometimes passes, sometimes fails.
Checklist:
- Are you awaiting the Promise?
// WRONG: Test completes before Promise resolves
@Test
void asyncTest() {
useCase.execute(request); // Returns Promise, not awaited!
}
// CORRECT: Await before asserting
@Test
void asyncTest() {
useCase.execute(request)
.await()
.onFailure(Assertions::fail);
}
- Is there shared mutable state between tests?
// WRONG: Shared state
private static List<String> logs = new ArrayList<>();
// CORRECT: Per-test state
private List<String> logs;
@BeforeEach
void setup() {
logs = new ArrayList<>();
}
- Are Promise timeouts too short?
// Increase timeout for CI environments
.await(TimeSpan.timeSpan(30).seconds())
Problem: âCanât create stub for step interfaceâ
Symptom: Compilation error when creating lambda stub.
// ERROR: Target type must be a functional interface
var stub = req -> Promise.success(req); // What type is this?
Fix: Declare the type explicitly:
// CORRECT: Type declaration
CheckEmail checkEmail = req -> Promise.success(req);
FAQ
General
Q: Is JBCT only for microservices?
A: No. JBCT works for any backend Java application - monoliths, microservices, serverless functions. The patterns are about code organization, not deployment topology.
Q: Does JBCT require Spring Boot?
A: No. JBCT is framework-agnostic. The examples use Spring Boot because itâs common, but the patterns work with Micronaut, Quarkus, plain Java, or any other framework.
Q: Can I use JBCT with Kotlin?
A: Yes, but consider Arrow-kt instead. Kotlinâs coroutines and extension functions provide more idiomatic alternatives. The concepts transfer directly.
Q: What about reactive streams (Project Reactor, RxJava)?
A: JBCTâs Promise is simpler than reactive streams. Use reactive streams when you need backpressure or complex stream operations. Use Promise for simple async request/response patterns.
Error Handling
Q: When should I use exceptions instead of Result?
A: Use exceptions for:
- Programming errors (IllegalArgumentException, NullPointerException)
- Unrecoverable system failures
- Framework requirements (Springâs @Transactional rollback)
Use Result for:
- Business failures (validation, not found, conflict)
- Expected error conditions
- Anything the caller might want to handle
Q: How do I handle errors from third-party libraries that throw?
A: Wrap at the adapter boundary:
public Promise<User> findUser(UserId id) {
return Promise.lift(
DatabaseError::new,
() -> jdbcTemplate.queryForObject(sql, mapper, id.value())
);
}
Q: Should every method return Result?
A: No. Pure functions that cannot fail should return T directly:
// Returns T - pure computation, cannot fail
public Money calculateTotal(List<LineItem> items) {
return items.stream()
.map(LineItem::subtotal)
.reduce(Money.ZERO, Money::add);
}
// Returns Result<T> - validation can fail
public Result<Money> parseAmount(String raw) {
return Money.money(raw);
}
Performance
Q: Is there overhead from wrapping everything in Result/Promise?
A: Minimal. The types are thin wrappers. Overhead is comparable to Optional. For typical web applications, this is negligible compared to I/O latency.
Q: Are lambdas slower than methods?
A: No. The JVM inlines simple lambdas. Thereâs no significant performance difference for typical use cases.
Q: Does creating many small objects (value objects) hurt GC?
A: Modern JVMs handle short-lived objects efficiently. Value objects are typically small and short-lived. If profiling shows GC pressure, the issue is usually elsewhere.
Testing
Q: Should I test value object validation separately from use cases?
A: Yes. Value objects get unit tests for each validation rule. Use cases get integration tests with stubbed steps.
Q: How many tests should a use case have?
A: At minimum:
- 1 happy path test
- 1 test per step failure
- 1 test per validation rule in ValidRequest
Q: Should I use mocks or stubs?
A: Prefer stubs (simple lambda implementations). Mocks add complexity and verification overhead. JBCTâs functional style makes stubbing easy.
Architecture
Q: Where do DTOs belong?
A: At adapter boundaries:
- Request DTOs in controller (convert to domain types immediately)
- Response DTOs from use case Response records
- No DTOs in domain layer
Q: Can use cases call other use cases?
A: Generally no. If use case A needs functionality from use case B, extract that functionality to a shared step interface.
Q: How do I handle transactions?
A: At the adapter layer:
@Repository
public class JooqOrderRepository implements SaveOrder {
@Transactional
@Override
public Promise<OrderId> apply(ValidOrder order) {
return Promise.lift(
DatabaseError::new,
() -> saveInTransaction(order)
);
}
}
Error Message Reference
âCannot infer functional interface typeâ
Cause: Lambda target type is ambiguous.
Fix: Add explicit type declaration:
CheckEmail checkEmail = req -> Promise.success(req);
âIncompatible types: Result<X> cannot be converted to Result<Y>â
Cause: Using map when flatMap is needed.
Fix: Check if the mapping function returns a Result. If so, use flatMap:
// WRONG: validate returns Result<ValidEmail>
.map(this::validate) // Results in Result<Result<ValidEmail>>
// CORRECT
.flatMap(this::validate) // Results in Result<ValidEmail>
âNo instances of type variable(s) T existâ
Cause: Type inference failure, often with Cause.result() or Cause.promise().
Fix: Add type witness or assign to typed variable:
// Option 1: Type witness
return ERROR.<User>result();
// Option 2: Assign to variable
Result<User> failure = ERROR.result();
return failure;
âPromise.await() blocks foreverâ
Cause: Promise never resolved.
Fix: Ensure all code paths resolve the Promise:
Promise<User> promise = Promise.promise();
// Ensure both success and failure paths resolve
if (condition) {
promise.succeed(user);
} else {
promise.fail(cause); // Don't forget this!
}
Summary
When debugging JBCT code:
- Add logging at chain points - Use onSuccess/onFailure for visibility
- Extract to named methods - Easier breakpoints and stack traces
- Check for common mistakes - Nested results, ignored returns, thrown exceptions
- Use explicit types - Helps IDE and compiler catch errors
- Test both success and failure - Always assert on Result/Promise
The functional style may feel unfamiliar initially, but debugging becomes easier once you internalize the patterns. The explicit error handling means fewer surprises at runtime.