// Java Backend Coding Technology skill for designing, implementing, and reviewing functional Java backend code. Use when working with Result, Option, Promise types, value objects, use cases, or when asked about JBCT patterns, monadic composition, parse-don't-validate, structural patterns (Leaf, Sequencer, Fork-Join), or functional Java backend architecture.
| name | JBCT |
| description | Java Backend Coding Technology skill for designing, implementing, and reviewing functional Java backend code. Use when working with Result, Option, Promise types, value objects, use cases, or when asked about JBCT patterns, monadic composition, parse-don't-validate, structural patterns (Leaf, Sequencer, Fork-Join), or functional Java backend architecture. |
A methodology for writing predictable, testable Java backend code optimized for human-AI collaboration.
Activate this skill when:
Result<T>, Option<T>, Promise<T> typesFor implementation work: Use jbct-coder subagent (Task tool with subagent_type: "jbct-coder")
For code review: Use jbct-reviewer subagent (Task tool with subagent_type: "jbct-reviewer")
JBCT reduces the space of valid choices to one good way to do most things through:
T, Option<T>, Result<T>, Promise<T>Cause values// T - Pure computation, cannot fail, always present
public String initials() { return ...; }
// Option<T> - May be absent, cannot fail
public Option<Theme> findTheme(UserId id) { return ...; }
// Result<T> - Can fail (validation/business errors)
public static Result<Email> email(String raw) { return ...; }
// Promise<T> - Asynchronous, can fail
public Promise<User> loadUser(UserId id) { return ...; }
Critical Rules:
Promise<Result<T>> - Promise already handles failuresVoid type - always use Unit (Result<Unit>, Promise<Unit>)Result.unitResult() for successful Result<Unit>// โ
CORRECT: Validation = Construction
public record Email(String value) {
private static final Fn1<Cause, String> INVALID_EMAIL =
Causes.forOneValue("Invalid email: %s");
public static Result<Email> email(String raw) {
return Verify.ensure(raw, Verify.Is::notNull)
.map(String::trim)
.flatMap(Verify.ensureFn(INVALID_EMAIL, Verify.Is::matches, PATTERN))
.map(Email::new);
}
}
// โ WRONG: Separate validation
public record Email(String value) {
public Result<Email> validate() { ... } // Don't do this
}
Key Points:
Email.email(...)Verify.Is Predicates - Use instead of custom lambdas:
Verify.Is::notNull // null check
Verify.Is::notBlank // non-empty, non-whitespace
Verify.Is::lenBetween // length in range
Verify.Is::matches // regex (String or Pattern)
Verify.Is::positive // > 0
Verify.Is::between // >= min && <= max
Verify.Is::greaterThan // > boundary
Parse Subpackage - Exception-safe JDK wrappers:
import org.pragmatica.lang.parse.Number;
import org.pragmatica.lang.parse.DateTime;
import org.pragmatica.lang.parse.Network;
Number.parseInt(raw) // Result<Integer>
DateTime.parseLocalDate(raw) // Result<LocalDate>
Network.parseUUID(raw) // Result<UUID>
Example:
public record Age(int value) {
public static Result<Age> age(String raw) {
return Number.parseInt(raw)
.flatMap(Verify.ensureFn(Causes.cause("Age 0-150"),
Verify.Is::between, 0, 150))
.map(Age::new);
}
}
public interface RegisterUser extends UseCase.WithPromise<Response, Request> {
record Request(String email, String password) {}
record Response(UserId userId, ConfirmationToken token) {}
// Nested API: steps as single-method interfaces
interface CheckEmail { Promise<ValidRequest> apply(ValidRequest valid); }
interface SaveUser { Promise<User> apply(ValidRequest valid); }
// Validated input with Valid prefix (not Validated)
record ValidRequest(Email email, Password password) {
static Result<ValidRequest> validRequest(Request raw) {
return Result.all(Email.email(raw.email()),
Password.password(raw.password()))
.map(ValidRequest::new);
}
}
// โ
CORRECT: Factory returns lambda directly
static RegisterUser registerUser(CheckEmail checkEmail, SaveUser saveUser) {
return request -> ValidRequest.validRequest(request)
.async()
.flatMap(checkEmail::apply)
.flatMap(saveUser::apply);
}
}
โ ANTI-PATTERN: Nested Record Implementation
NEVER create factories with nested record implementations:
// โ WRONG - Verbose, no benefit
static RegisterUser registerUser(CheckEmail check, SaveUser save) {
record registerUser(CheckEmail check, SaveUser save) implements RegisterUser {
@Override
public Promise<Response> execute(Request request) { ... }
}
return new registerUser(check, save);
}
Rule: Records are for data (value objects), lambdas are for behavior (use cases, steps).
Core Rules:
Pattern-Specific Safety:
Example - Thread-Safe Fork-Join:
// โ
CORRECT: Immutable cart passed to both operations
Promise.all(applyBogo(cart), // cart is immutable
applyPercentOff(cart)) // cart is immutable
.map(this::mergeDiscounts);
// โ WRONG: Shared mutable context creates data race
private final DiscountContext context = new DiscountContext();
Promise.all(applyBogo(cart, context), // mutates context
applyPercentOff(cart, context)) // DATA RACE
.map(this::merge);
See CODING_GUIDE.md for comprehensive thread safety coverage, including detailed examples and common mistakes.
Rule: Lambdas passed to monadic operations (map, flatMap, recover, filter) must be minimal.
Allowed:
Email::new, this::processUser, User::iduser -> validate(requiredRole, user)RepositoryError.DatabaseFailure::newForbidden:
if, ternary, switch)Pattern matching: Use switch expressions in named methods:
// Extract type matching to named method
.recover(this::recoverKnownErrors)
private Promise<T> recoverKnownErrors(Cause cause) {
return switch (cause) {
case NotFound ignored, Timeout ignored -> DEFAULT.promise();
default -> cause.promise();
};
}
Multi-case matching: Comma-separated for same recovery:
private Promise<Theme> recoverWithDefault(Cause cause) {
return switch (cause) {
case NotFound ignored, Timeout ignored, ServiceUnavailable ignored ->
Promise.success(Theme.DEFAULT);
default -> cause.promise();
};
}
Error constants: Define once, reuse everywhere:
private static final Cause NOT_FOUND = new UserNotFound("User not found");
private static final Cause TIMEOUT = new ServiceUnavailable("Request timed out");
private Promise<User> recoverNetworkError(Cause cause) {
return switch (cause) {
case NetworkError.Timeout ignored -> TIMEOUT.promise();
default -> cause.promise();
};
}
Atomic unit - single responsibility, no composition:
public Promise<User> findUser(UserId id) {
return Promise.lift(
RepositoryError.DatabaseFailure::new,
() -> jdbcTemplate.queryForObject(...)
);
}
Linear dependent steps (most common use case pattern):
return ValidRequest.validRequest(request)
.async()
.flatMap(checkEmail::apply)
.flatMap(hashPassword::apply)
.flatMap(saveUser::apply)
.flatMap(sendEmail::apply);
Parallel independent operations (requires immutable inputs):
return Promise.all(fetchProfile.apply(userId),
fetchPreferences.apply(userId),
fetchOrders.apply(userId))
.map((profile, prefs, orders) ->
new Dashboard(profile, prefs, orders));
Thread Safety: All parallel operations must receive immutable inputs. No shared mutable state.
Branching as values (no mutation):
return userType.equals("premium")
? processPremium.apply(request)
: processBasic.apply(request);
Functional collection processing:
var results = items.stream()
.map(Item::validate)
.toList();
return Result.allOf(results)
.map(validItems -> process(validItems));
Cross-cutting concerns without mixing:
return withRetry(
retryPolicy,
withMetrics(metricsPolicy, coreOperation)
);
// Option โ Result/Promise
option.toResult(cause) // or .await(cause)
option.async(cause)
// Result โ Promise
result.async()
// Promise โ Result (blocking)
promise.await()
promise.await(timeout)
// Cause โ Result/Promise (prefer over failure constructors)
cause.result()
cause.promise()
// Result.all - Accumulates all failures
Result.all(result1, result2, result3)
.map((v1, v2, v3) -> combine(v1, v2, v3));
// Promise.all - Fail-fast on first failure
Promise.all(promise1, promise2, promise3)
.map((v1, v2, v3) -> combine(v1, v2, v3));
// Option.all - Fail-fast on first empty
Option.all(opt1, opt2, opt3)
.map((v1, v2, v3) -> combine(v1, v2, v3));
// Lift exceptions in adapters
Promise.lift(
RepositoryError.DatabaseFailure::new,
() -> jdbcTemplate.queryForObject(...)
);
// With custom exception mapper (constructor reference preferred)
Result.lift(
CustomError.ProcessingFailed::new,
() -> riskyOperation()
);
TypeName.typeName(...) (lowercase-first)Valid prefix (not Validated): ValidRequest, ValidUserEmailNotFound, AccountLocked, PaymentFailedmethodName_outcome_conditionhttpClient, apiKey not HTTPClient, APIKeySource: Adapted from Derrick Brandt's systematic approach.
Use zone-appropriate verbs to maintain consistent abstraction levels:
Zone 2 (Step Interfaces - Orchestration):
validate, process, handle, transform, apply, check, load, save, manage, configure, initializeValidateInput, ProcessPayment, HandleRefund, LoadUserDataZone 3 (Leaves - Implementation):
get, set, fetch, parse, calculate, convert, hash, format, encode, decode, extract, split, join, log, send, receive, read, write, add, removehashPassword(), parseJson(), fetchFromDatabase(), calculateTax()Anti-pattern: Mixing zones (e.g., step interface named FetchUserData uses Zone 3 verb fetch instead of Zone 2 verb load)
Stepdown rule test: Read code aloud with "to" before functions - should flow naturally:
// "To execute, we validate the request, then process payment, then send confirmation"
return ValidRequest.validRequest(request)
.async()
.flatMap(this::processPayment)
.flatMap(this::sendConfirmation);
For complete zone verb vocabulary, see CODING_GUIDE.md: Zone-Based Naming Vocabulary.
com.example.app/
โโโ usecase/
โ โโโ registeruser/ # Self-contained vertical slice
โ โ โโโ RegisterUser.java # Use case interface + factory
โ โ โโโ [internal types] # ValidRequest, etc.
โ โโโ loginuser/
โ โโโ LoginUser.java
โโโ domain/
โ โโโ shared/ # Reusable value objects ONLY
โ โโโ Email.java
โ โโโ Password.java
โ โโโ UserId.java
โโโ adapter/
โโโ rest/ # Inbound (HTTP)
โโโ persistence/ # Outbound (DB)
โโโ messaging/ # Outbound (queues)
Placement Rules:
domain/shared/Error Structure (General enum pattern):
public sealed interface RegistrationError extends Cause {
// Group fixed-message errors into single enum
enum General implements RegistrationError {
EMAIL_ALREADY_REGISTERED("Email already registered"),
WEAK_PASSWORD_FOR_PREMIUM("Premium codes require 10+ char passwords");
private final String message;
General(String message) { this.message = message; }
@Override public String message() { return message; }
}
// Records for errors with data (e.g., Throwable)
record PasswordHashingFailed(Throwable cause) implements RegistrationError {
@Override public String message() { return "Password hashing failed"; }
}
}
// Usage
RegistrationError.General.EMAIL_ALREADY_REGISTERED.promise()
// Test failures - use .onSuccess(Assertions::fail)
@Test
void validation_fails_forInvalidInput() {
ValidRequest.validRequest(new Request("invalid", "bad"))
.onSuccess(Assertions::fail);
}
// Test successes - chain onFailure then onSuccess
@Test
void validation_succeeds_forValidInput() {
ValidRequest.validRequest(new Request("valid@example.com", "Valid1234"))
.onFailure(Assertions::fail)
.onSuccess(valid -> {
assertEquals("valid@example.com", valid.email().value());
});
}
// Async tests - use .await() first
@Test
void execute_succeeds_forValidInput() {
useCase.execute(request)
.await()
.onFailure(Assertions::fail)
.onSuccess(response -> {
assertEquals("expected", response.value());
});
}
JBCT uses Pragmatica Lite Core 0.8.3 for functional types.
Maven (preferred):
<dependency>
<groupId>org.pragmatica-lite</groupId>
<artifactId>core</artifactId>
<version>0.8.3</version>
</dependency>
Gradle (only if explicitly requested):
implementation 'org.pragmatica-lite:core:0.8.3'
Library documentation: https://central.sonatype.com/artifact/org.pragmatica-lite/core
This skill provides quick reference and learning resources. For complex implementation and review tasks, use specialized subagents:
How to invoke: Use Task tool with subagent_type: "jbct-coder"
What it provides:
Result.all()How to invoke: Use Task tool with subagent_type: "jbct-reviewer"
What it provides:
Result.all()๐ก Tip: For automatic generation following this workflow, use the jbct-coder subagent.
โ Using business exceptions instead of Result/Promise
โ Nested records in use case factories (use lambdas)
โ Void type (use Unit)
โ Promise<Result<T>> (redundant nesting)
โ Separate validation methods (parse at construction)
โ Public constructors on value objects
โ Complex logic in lambdas (extract to methods)
โ Validated prefix (use Valid)
๐ก Tip: For automated code review checking these mistakes, use the jbct-reviewer subagent.
This skill contains comprehensive guidance organized by topic:
Repository: https://github.com/siy/coding-technology