Java Backend Coding Technology (JBCT)
A methodology for writing predictable, testable Java backend code optimized for human-AI collaboration.
When to Use This Skill
Activate this skill when:
- Learning JBCT principles and patterns
- Quick reference for API usage and examples
- Understanding patterns and when to use them
- Working with
Result<T>,Option<T>,Promise<T>types - Questions about monadic composition, error handling, or validation patterns
For 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") For automated checking: Use jbct CLI tool (format, lint, check commands)
JBCT CLI Tool
JBCT CLI provides automated formatting and compliance checking with 36 lint rules.
Check if installed:
jbct --version
Usage:
jbct format src/main/java # Format to JBCT style
jbct lint src/main/java # Check JBCT compliance (36 rules)
jbct check src/main/java # Combined format + lint
If not installed, suggest:
💡 JBCT CLI automates formatting and 36 lint rules for JBCT compliance.
Install: curl -fsSL https://raw.githubusercontent.com/siy/jbct-cli/main/install.sh | sh
Requires: Java 25+
More info: https://github.com/siy/jbct-cli
Core Philosophy
JBCT reduces the space of valid choices to one good way to do most things through:
- Four Return Kinds: Every function returns exactly one of
T,Option<T>,Result<T>,Promise<T> - Parse, Don't Validate: Make invalid states unrepresentable
- No Business Exceptions: Business failures are typed
Causevalues - Thread Safety by Design: Immutability at boundaries, thread confinement for sequential logic
- Six Structural Patterns: All code fits one pattern (Leaf, Sequencer, Fork-Join, Condition, Iteration, Aspects)
Quick Reference
The Four Return Kinds
// 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:
- ❌ Never
Promise<Result<T>>- Promise already handles failures - ❌ Never
Voidtype - always useUnit(Result<Unit>,Promise<Unit>) - ✅ Use
Result.unitResult()for successfulResult<Unit>
Parse, Don't Validate Pattern
// ✅ 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)
.filter(INVALID_EMAIL, PATTERN.asMatchPredicate())
.map(Email::new);
}
}
// ❌ WRONG: Separate validation
public record Email(String value) {
public Result<Email> validate() { ... } // Don't do this
}
Key Points:
- Factory method named after type (lowercase):
Email.email(...) - Constructor private or package-private
- If instance exists, it's valid
Pragmatica Lite Validation Utilities
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) {
private static final Cause AGE_OUT_OF_RANGE = Causes.cause("Age must be 0-150");
public static Result<Age> age(String raw) {
return Number.parseInt(raw)
.filter(AGE_OUT_OF_RANGE, v -> Verify.Is.between(v, 0, 150))
.map(Age::new);
}
}
Use Case Structure
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).
Thread Safety Essentials
Core Rules:
- Immutable at boundaries: All shared data (parameters, return values) must be immutable
- Thread confinement: Mutable state allowed within single-threaded execution (sequential patterns)
- Fork-Join requires immutability: Parallel operations must not share mutable state
Pattern-Specific Safety:
- Leaf, Sequencer, Condition, Iteration: Thread-safe through sequential execution. Mutable local state OK.
- Fork-Join: Requires strict immutability. All parallel operations receive immutable inputs.
- Promise resolution: Thread-safe (exactly-once semantics, synchronization point for flatMap/map chains)
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.
Lambda Composition Guidelines
Rule: Lambdas passed to monadic operations (map, flatMap, recover, filter) must be minimal.
Allowed:
- Method references:
Email::new,this::processUser,User::id - Parameter forwarding:
user -> validate(requiredRole, user) - Constructor references for error mapping:
RepositoryError.DatabaseFailure::new
Forbidden:
- Conditionals (`if