Part 10: Systematic Application Guide
Series: Java Backend Coding Technology | Part: 10 of 10
Previous: Part 9: Production Systems | Complete Series: Index
Overview
Youβve learned the principles, patterns, and testing approaches. Youβve seen complete production systems. Now we need a systematic way to apply JBCT consistently during coding and review.
This part introduces checkpoints - specific moments during coding where you pause and verify compliance. This prevents violations from accumulating and reduces review/fix loops.
By the end of this part, youβll have:
- 8 checkpoints covering every aspect of JBCT
- Quick reference tables for immediate lookup
- Violation β Fix patterns for common mistakes
- Application order for new code and reviews
The Checkpoint Approach
JBCT application works through checkpoints - specific moments during coding where you pause and verify compliance. Each checkpoint has:
- Trigger: When to apply this checkpoint
- Rules: What to check
- Fixes: How to correct violations
The goal is 100% compliance through systematic verification, not heroic effort.
CHECKPOINT 1: Before Writing Any Lambda
Trigger: Youβre about to write ->
Rules to Check
| Rule | Check | Fix |
|---|---|---|
| L1 | Is method reference possible? | Use Type::method instead of x -> x.method() |
| L2 | Does lambda have braces {}? |
Extract to named method |
| L3 | Are there nested monadic operations inside? | Extract inner operation to separate method |
| L4 | Is there control flow (if/switch/try)? | Extract to named method |
| L5 | Multiple statements? | Extract to named method |
Allowed Lambda Forms (Exhaustive)
// Method references (ALWAYS PREFERRED)
.map(Email::new)
.flatMap(this::validate)
// Single-value expression (no braces)
.map(value -> expression)
.filter(s -> !s.isBlank())
// Multi-value expression (no braces)
.map((a, b) -> new Pair(a, b))
Extraction Pattern
// BEFORE (violation)
.map(data -> {
cache.put(key, data);
log.info("Cached: {}", data);
return data.size();
})
// AFTER (compliant)
.map(this::cacheAndCount)
private int cacheAndCount(Data data) {
cache.put(key, data);
log.info("Cached: {}", data);
return data.size();
}
CHECKPOINT 2: Choosing Return Type
Trigger: Writing a method signature
Decision Tree
Can this operation fail?
βββ NO: Can the value be absent?
β βββ NO β return T
β βββ YES β return Option<T>
βββ YES: Is it async/IO?
βββ NO β return Result<T>
βββ YES β return Promise<T>
Rules to Check
| Rule | Check | Fix |
|---|---|---|
| R1 | Does Result always succeed? | Change to T |
| R2 | Is Option always present? | Change to T |
| R3 | Using Promise<Result |
Use Promise |
| R4 | Returning Void? | Use Unit |
| R5 | Returning null? | Use Option |
Anti-pattern Detection
// VIOLATION: Result that never fails
public static Result<Config> config(...) {
return Result.success(new Config(...)); // Always succeeds!
}
// FIX: Return T directly
public static Config config(...) {
return new Config(...);
}
CHECKPOINT 3: Writing Factory Methods
Trigger: Creating construction logic for a type
Rules to Check
| Rule | Check | Fix |
|---|---|---|
| F1 | Name follows TypeName.typeName()? |
Rename to lowercase-first |
| F2 | Validation happens at construction? | Move validation into factory |
| F3 | Return type matches validation needs? | Apply Checkpoint 2 |
| F4 | Constructor exposed publicly? | Make factory the only entry point |
Pattern
public record Email(String value) {
// Factory with validation β Result<T>
public static Result<Email> email(String raw) {
return Verify.ensure(raw, Verify.Is::notNull)
.map(String::trim)
.filter(INVALID_EMAIL, s -> PATTERN.matcher(s).matches())
.map(Email::new);
}
}
public record Config(DbUrl url, DbPassword pass) {
// Factory without validation (fields pre-validated) β T
public static Config config(DbUrl url, DbPassword pass) {
return new Config(url, pass);
}
}
CHECKPOINT 4: Designing a Class/Interface
Trigger: Creating a new type
Rules to Check
| Rule | Check | Fix |
|---|---|---|
| D1 | What zone does this belong to? | Place in correct package |
| D2 | Does it mix I/O with domain logic? | Split into separate types |
| D3 | Are primitives used for domain concepts? | Extract value objects |
| D4 | Does naming match the zone? | Adjust naming style |
Zone Placement
Zone A (Entry): Controllers, handlers, main
β Business action verbs: handleRegistration(), processOrder()
Zone B (Domain): Use cases, value objects, domain services
β Domain vocabulary: Email.email(), ValidRequest.validRequest()
Zone C (Infrastructure): DB, external APIs, config loading
β Technical names: loadAllGenerations(), saveUser(), readFile()
Mixed Responsibility Detection
// VIOLATION: Domain entity with I/O
public record ExtensionConfig(...) {
public static Result<ExtensionConfig> load(Path file) {
return Files.readString(file) // I/O in domain!
.flatMap(this::parse);
}
}
// FIX: Separate concerns
public record ExtensionConfig(...) { } // Pure domain (Zone B)
public interface ConfigLoader { // I/O adapter (Zone C)
static Result<ExtensionConfig> load(Path file) { ... }
}
CHECKPOINT 5: Writing Monadic Chains
Trigger: Chaining .map()/.flatMap()/.filter() etc.
Rules to Check
| Rule | Check | Fix |
|---|---|---|
| M1 | Single pattern per method? | Extract mixed patterns |
| M2 | Chain length β€ 5 steps? | Split into composed methods |
| M3 | Side effects only in terminal ops? | Move to .onSuccess()/.onFailure() |
| M4 | Logging mixed with logic? | Move logging to appropriate layer |
Pattern Separation
// VIOLATION: Mixing Sequencer + Fork-Join
return validate(request)
.flatMap(req -> Result.all(
checkInventory(req),
validatePayment(req)
).map((inv, pay) -> proceed(req)));
// FIX: Extract Fork-Join
return validate(request)
.flatMap(this::validateOrder)
.flatMap(this::processOrder);
private Result<ValidRequest> validateOrder(ValidRequest req) {
return Result.all(checkInventory(req), validatePayment(req))
.map((inv, pay) -> req);
}
CHECKPOINT 6: Adding Logging
Trigger: Adding log statements
Rules to Check
| Rule | Check | Fix |
|---|---|---|
| G1 | Is logging conditional on data? | Remove condition, use log level |
| G2 | Logger passed as parameter? | Move logging to owning component |
| G3 | Logging in pure transformation? | Move to terminal operation |
| G4 | Duplicate logging across layers? | Single responsibility - one layer logs |
Anti-pattern
// VIOLATION: Conditional logging
if (count > 0) {
log.debug("Processed {} items", count);
}
// FIX: Unconditional, let log config filter
log.debug("Processed {} items", count);
Ownership Pattern
// VIOLATION: Caller logs for callee
cache.refresh()
.onSuccess(count -> log.debug("Refreshed {}", count))
.onFailure(cause -> log.error("Failed: {}", cause));
// FIX: Cache owns its logging
// In GenerationCache:
public Result<Integer> refresh() {
return doRefresh()
.onSuccess(count -> log.debug("Refreshed {}", count))
.onFailure(cause -> log.error("Failed: {}", cause));
}
// Caller just invokes:
cache.refresh();
CHECKPOINT 7: Review Completeness
Trigger: Before submitting code for review / finishing implementation
Verification Checklist
- [ ] Every lambda checked against L1-L5
- [ ] Every return type checked against R1-R5
- [ ] Every factory method checked against F1-F4
- [ ] Every new type checked against D1-D4
- [ ] Every monadic chain checked against M1-M4
- [ ] Every log statement checked against G1-G4
- [ ] No FQCNs in code (use imports)
- [ ] Test names follow
methodName_outcome_condition
CHECKPOINT 8: Implementation Patterns
Trigger: Choosing implementation style for interfaces
Rule I1: Nested Record vs Lambda Pattern
Decision tree:
Does implementation need state beyond parameters?
βββ YES β Use nested record pattern
β (Records capture state explicitly as fields)
βββ NO: Is it a functional interface (single method)?
βββ YES β Use lambda pattern
βββ NO β Use nested record pattern
Nested record pattern (when state is needed):
public interface GenerationCache {
Result<Unit> refresh();
Generation getOrDefaultNg(String clientId);
static GenerationCache generationCache(WorkConfig config) {
record generationCache(
AtomicReference<Instant> lastRefreshTime, // State
ConcurrentMap<String, Generation> cache, // State
WorkConfig config,
Logger log
) implements GenerationCache {
@Override
public Result<Unit> refresh() { ... }
@Override
public Generation getOrDefaultNg(String clientId) { ... }
}
return new generationCache(
new AtomicReference<>(Instant.EPOCH),
new ConcurrentHashMap<>(),
config,
LoggerFactory.getLogger(GenerationCache.class)
);
}
}
Lambda pattern (for functional interfaces, no state):
public interface Step {
Result<Data> execute(Context ctx);
static Step validate(Validator validator) {
return ctx -> validator.validate(ctx.input()); // Pure lambda
}
}
Rule I2: Conditional Option Composition
Trigger: Creating Option based on condition + value extraction
Anti-pattern (imperative mess):
var matcher = pattern.matcher(topic);
var matches = matcher.matches();
var clientId = matcher.group(1); // BUG: called before check!
var isOg = generation == Generation.OG;
return (matches && isOg)
? Option.option(clientId)
: Option.none();
Correct pattern (functional, short-circuiting):
var matcher = pattern.matcher(topic);
return Option.option(matcher.matches() ? matcher.group(1) : null)
.filter(clientId -> cache.getOrDefaultNg(clientId) == Generation.OG);
Benefits:
- Safe: value extracted only when condition true
- Short-circuits: filter not evaluated if Option is empty
- Composable: can chain more filters/maps
- Clear data flow: reads top-to-bottom
Rule I3: Validation Ownership
Core principle: Validation (parsing) ALWAYS lives in value objects.
Rules:
- Never use raw types if ANY validation is needed
- If valid range doesnβt exactly match raw type β create Value Object
- Validation logic exists in ONE place: the value object factory
Anti-pattern (duplicate validation):
// In transformer - WRONG
private Result<String> validateAction(String action) {
return "open".equals(action) || "close".equals(action)
? Result.success(action)
: UNKNOWN_COMMAND.result();
}
private Result<Command> buildCommand(String[] fields) {
return validateAction(fields[0]) // Duplicate!
.flatMap(action -> Command.command(action, fields[1]));
}
Correct pattern (single ownership):
// In Command value object - validation lives here
public static Result<Command> command(String action, String target) {
return validateAction(action)
.map(_ -> new Command(action, Option.empty(), target));
}
// In transformer - just delegate
private Result<Command> buildCommand(String[] fields) {
return Command.command(fields[0], fields[1]); // Trust the value object
}
Rule I4: Utility Class Pattern
Standard pattern: sealed interface with unused record
public sealed interface InterceptorUtils {
static Option<String> matches(String topic, Pattern pattern) { ... }
static String bufferToString(ByteBuffer buffer) { ... }
record unused() implements InterceptorUtils {}
}
Visibility:
- If used only within same package β package-private, lives with consumers
- If shared across packages β public, lives in
sharedpackage under longest common package path
Quick Reference: Violation β Fix Patterns
| Violation | Detection | Fix |
|---|---|---|
| Multi-statement lambda | { } with multiple lines |
Extract to method |
| Nested monadic ops | .flatMap(x -> y.map(...)) |
Extract inner to method |
| Always-succeeding Result | Result.success(new X()) |
Return X directly |
| Mixed I/O and domain | File/DB ops in domain class | Split to adapter |
| Primitive obsession | String url, int poolSize |
Create value object |
| Conditional logging | if (x) log.debug() |
Remove condition |
| Logger as parameter | method(Logger log) |
Move logging to owner |
| FQCN in code | org.foo.Bar in method body |
Add import |
Application Order
When Implementing New Code
- Design phase: Apply Checkpoint 4 (class design)
- Signature phase: Apply Checkpoint 2 (return types), Checkpoint 3 (factories)
- Implementation phase: Apply Checkpoint 1 (lambdas), Checkpoint 5 (chains), Checkpoint 8 (patterns)
- Polish phase: Apply Checkpoint 6 (logging)
- Completion phase: Apply Checkpoint 7 (review)
When Reviewing Existing Code
- Scan for lambdas (
->) - apply Checkpoint 1 - Scan for return types - apply Checkpoint 2
- Scan for factories - apply Checkpoint 3
- Scan for mixed responsibilities - apply Checkpoint 4
- Scan for logging - apply Checkpoint 6
- Final verification - apply Checkpoint 7
Summary
Systematic application of JBCT through checkpoints ensures:
- Consistency: Same code structure regardless of who writes it
- Efficiency: Violations caught early, not in review
- Quality: 100% compliance is achievable through systematic verification
The checkpoints form a complete verification system:
- Checkpoints 1-3: Code mechanics (lambdas, types, factories)
- Checkpoints 4-5: Structure (classes, chains)
- Checkpoint 6: Cross-cutting (logging)
- Checkpoint 7: Final verification
- Checkpoint 8: Implementation patterns
Apply them systematically, and JBCT compliance becomes automatic.
Whatβs Next?
This completes the JBCT learning series. You now have:
- Foundations (Part 1) - Mental model and core ideas
- Four Return Types (Part 2) - T, Option, Result, Promise
- Parse, Donβt Validate (Part 3) - Making invalid states unrepresentable
- Error Handling (Part 4) - Errors as values, composition rules
- Basic Patterns (Part 5) - Leaf, Condition, Iteration
- Advanced Patterns (Part 6) - Sequencer, Fork-Join, Aspects
- Testing Philosophy (Part 7) - Integration-first approach
- Testing Practice (Part 8) - Organization and examples
- Production Systems (Part 9) - Complete walkthrough
- Systematic Application (Part 10) - Checkpoints for coding and review
For reference: See CODING_GUIDE.md for the complete technical documentation.
For AI assistance: Use the jbct-coder and jbct-reviewer subagents for Claude Code integration.
End of Series