Chapter 19: Comparison with Other Approaches
JBCT doesnβt exist in isolation. This chapter compares it to other architectural approaches you may encounter, helping you understand where JBCT fits and what it borrows from or rejects in other methodologies.
Traditional Layered Architecture
The most common Java backend architecture.
Structure
Controller Layer β Receives HTTP requests, validates DTOs
β
Service Layer β Business logic, transactions, orchestration
β
Repository Layer β Database access, queries
β
Entity Layer β JPA entities, data structures
Example
@RestController
public class UserController {
@Autowired private UserService userService;
@PostMapping("/users")
public ResponseEntity<UserDto> createUser(@Valid @RequestBody CreateUserDto dto) {
User user = userService.createUser(dto);
return ResponseEntity.ok(toDto(user));
}
}
@Service
public class UserService {
@Autowired private UserRepository userRepository;
@Autowired private PasswordEncoder passwordEncoder;
@Transactional
public User createUser(CreateUserDto dto) {
if (userRepository.existsByEmail(dto.getEmail())) {
throw new EmailExistsException();
}
User user = new User();
user.setEmail(dto.getEmail());
user.setPassword(passwordEncoder.encode(dto.getPassword()));
return userRepository.save(user);
}
}
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
boolean existsByEmail(String email);
}
What JBCT Changes
| Aspect | Layered | JBCT |
|---|---|---|
| Error handling | Exceptions | Result/Promise with Cause |
| Validation | @Valid + manual checks | Parse-donβt-validate |
| Business logic location | Service layer | Use case interface |
| Data flow | Mutable entities | Immutable value objects |
| Dependencies | Field injection | Constructor injection via factory |
What JBCT Keeps
- Separation of concerns (different responsibility per layer)
- Controllers at the boundary
- Repository pattern for data access
Verdict
JBCT refines layered architecture rather than replacing it. The layers still exist, but with explicit error handling and immutable data flow.
Hexagonal Architecture (Ports and Adapters)
Popularized by Alistair Cockburn. Core idea: business logic at the center, adapters at the edges.
Structure
βββββββββββββββββββββββ
HTTP Adapter βββββ βββββ Database Adapter
β Domain / Core β
Queue Adapter βββββ (Business Logic) βββββ External API Adapter
β β
βββββββββββββββββββββββ
β β
Ports (Interfaces)
Example
// Port (interface defined by domain)
public interface UserRepository {
Optional<User> findByEmail(Email email);
void save(User user);
}
// Domain service (pure business logic)
public class UserRegistrationService {
private final UserRepository userRepository;
private final PasswordHasher passwordHasher;
public User register(Email email, Password password) {
if (userRepository.findByEmail(email).isPresent()) {
throw new EmailAlreadyExistsException(email);
}
HashedPassword hashed = passwordHasher.hash(password);
User user = new User(email, hashed);
userRepository.save(user);
return user;
}
}
// Adapter (implements port)
public class JpaUserRepository implements UserRepository {
private final JpaUserEntityRepository jpaRepo;
@Override
public Optional<User> findByEmail(Email email) {
return jpaRepo.findByEmail(email.value())
.map(this::toDomain);
}
}
What JBCT Borrows
- Ports concept β Step interfaces
- Adapters concept β Adapter leaves
- Domain at center β Use cases with pure business logic
- Dependency inversion β Steps injected into use case factory
What JBCT Adds
- Explicit error handling - Hexagonal doesnβt prescribe how to handle errors
- Functional composition - Hexagonal uses imperative style
- Typed failures - Cause types instead of exceptions
- Structural patterns - Leaf, Sequencer, Fork-Join provide composition vocabulary
Key Difference
Hexagonal focuses on where code lives (inside vs outside the hexagon). JBCT focuses on how code composes (patterns, error handling, data flow).
Verdict
JBCT and Hexagonal are complementary. Use Hexagonal for high-level architecture, JBCT for implementation patterns within that architecture.
Clean Architecture
Uncle Bobβs architecture with explicit dependency rules.
Structure
βββββββββββββββββββββββββββββββββββββββββββββββββ
β Frameworks & Drivers β
β βββββββββββββββββββββββββββββββββββββββββ β
β β Interface Adapters β β
β β βββββββββββββββββββββββββββββββββ β β
β β β Application Layer β β β
β β β βββββββββββββββββββββββββ β β β
β β β β Domain Layer β β β β
β β β β (Entities) β β β β
β β β βββββββββββββββββββββββββ β β β
β β βββββββββββββββββββββββββββββββββ β β
β βββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββ
Dependencies point inward. Inner layers donβt know about outer layers.
Example
// Entity (innermost)
public class User {
private UserId id;
private Email email;
private HashedPassword password;
public boolean canLogin(Password attempt, PasswordHasher hasher) {
return hasher.verify(attempt, this.password);
}
}
// Use Case (application layer)
public class RegisterUserUseCase {
private final UserGateway userGateway;
private final PasswordHasher passwordHasher;
public RegisterUserResponse execute(RegisterUserRequest request) {
Email email = new Email(request.email);
if (userGateway.existsByEmail(email)) {
return RegisterUserResponse.failure("Email exists");
}
// ... rest of use case
}
}
// Gateway interface (application layer, implemented by adapter)
public interface UserGateway {
boolean existsByEmail(Email email);
void save(User user);
}
What JBCT Borrows
- Use case as first-class concept β UseCase interface
- Dependency rule β Use cases donβt depend on adapters
- Request/Response models β Request/ValidRequest/Response records
What JBCT Changes
| Aspect | Clean Architecture | JBCT |
|---|---|---|
| Use case return | Response objects | Result/Promise |
| Error handling | Response codes or exceptions | Typed Cause |
| Entity behavior | Rich domain model | Value objects + pure functions |
| Composition | Imperative orchestration | Monadic chains |
Key Difference
Clean Architecture prescribes dependency direction but not composition style. JBCT prescribes both.
Verdict
JBCT can be implemented within Clean Architecture. The layers map naturally:
- Entities β Value objects
- Use Cases β Use case interfaces
- Interface Adapters β Adapter leaves
- Frameworks β Spring configuration
Railway-Oriented Programming
Scott Wlaschinβs approach from F#, using βtwo-trackβ types for error handling.
Concept
Success Track: βββββββββββββββββββββββββββββββ β Success
β β β β
β β β β
Failure Track: ββββββββββββββββββββββββββββββββ Failure
Each function either stays on success track or switches to failure track.
Example (F# style in Java)
public Result<User> registerUser(String email, String password) {
return validateEmail(email) // Success or Failure
.flatMap(this::validatePassword) // Continue or stay failed
.flatMap(this::checkUniqueness) // Continue or stay failed
.flatMap(this::createUser); // Continue or stay failed
}
What JBCT Borrows
- Two-track model β
Result<T>is exactly this - flatMap for composition β Same chaining style
- Errors as values β Cause instead of exceptions
What JBCT Adds
- Four tracks, not two - T, Option, Result, Promise for different semantics
- Structural patterns - Named patterns (Sequencer, Fork-Join) beyond just chaining
- Aggregation - Result.all() for parallel validation
- Async integration - Promise extends the model to async operations
Key Difference
ROP is a technique for error handling. JBCT is a complete methodology including project structure, testing, and team practices.
Verdict
JBCT incorporates ROP as its error handling model, then builds a full methodology around it.
vavr (formerly Javaslang)
Functional programming library for Java.
What vavr Provides
import io.vavr.control.Try;
import io.vavr.control.Either;
import io.vavr.control.Option;
// Try - captures exceptions
Try<Integer> result = Try.of(() -> Integer.parseInt(input));
// Either - success or failure with typed error
Either<Error, User> user = findUser(id);
// Option - presence or absence
Option<User> maybeUser = Option.of(nullableUser);
// Pattern matching
String message = Match(result).of(
Case($Success($()), "Parsed"),
Case($Failure($()), "Failed")
);
Comparison
| Feature | vavr | Pragmatica Core |
|---|---|---|
| Option type | Option<T> |
Option<T> |
| Error type | Either<L, R> or Try<T> |
Result<T> with Cause |
| Async type | None (use CompletableFuture) | Promise<T> |
| Collections | Immutable collections | Uses Java collections |
| Pattern matching | Built-in DSL | Standard switch expressions |
| Tuples | Tuple1-8 | Use records |
Key Differences
-
vavr is a library, JBCT is a methodology - vavr provides types, JBCT provides patterns, structure, and practices.
-
Error typing - vavrβs Either has generic left type. Pragmatica Coreβs Result always uses Cause, providing consistent error handling.
-
Async story - vavr doesnβt provide async primitives. JBCTβs Promise integrates error handling with async operations.
-
Simplicity - vavr includes many FP features (persistent collections, pattern matching DSL, streams). Pragmatica Core focuses on the minimum needed for JBCT.
When to Use vavr
- You want immutable collections
- Youβre building a library with FP patterns
- You need persistent data structures
- You want pattern matching DSL
When to Use Pragmatica Core
- Youβre building backend services
- You want a complete methodology (not just types)
- You need integrated async support
- You prefer simplicity over features
Verdict
vavr is a more comprehensive FP library. Pragmatica Core is purpose-built for JBCT. You could use vavr to implement JBCT patterns, but youβd need to add your own Promise type and establish your own structural patterns.
Arrow-kt (Kotlin)
Functional programming library for Kotlin.
What Arrow Provides
// Either for errors
fun divide(a: Int, b: Int): Either<DivisionError, Int> =
if (b == 0) DivisionError.DivideByZero.left()
else (a / b).right()
// Validated for accumulating errors
fun validateUser(name: String, age: Int): ValidatedNel<ValidationError, User> =
ValidatedNel.applicative<ValidationError>().mapN(
validateName(name),
validateAge(age)
) { (n, a) -> User(n, a) }
// Effect for async
suspend fun fetchUser(id: UserId): Either<Error, User> =
either { userRepository.findById(id).bind() }
Why Mention It?
Arrow-kt demonstrates that these patterns work well in a JVM language. Key insights:
- Kotlinβs coroutines + Either = Similar to
Promise<T> - Validated type = Similar to Result.all() accumulation
- Typed errors = Same as JBCTβs Cause
Verdict
If youβre on Kotlin, Arrow-kt provides similar capabilities to JBCT. The concepts transfer, but the syntax differs due to language features (coroutines, extension functions, sealed classes).
Summary: Where JBCT Fits
Architectural Style
β
ββββββββββββββββββββΌβββββββββββββββββββ
β β β
Layered Hexagonal Clean
β β β
ββββββββββββββββββββΌβββββββββββββββββββ
β
Implementation Style
β
ββββββββββββββββββββΌβββββββββββββββββββ
β β β
Imperative Railway (ROP) FP Library
(exceptions) (Result types) (vavr, Arrow)
β β β
ββββββββββββββββββββΌβββββββββββββββββββ
β
JBCT
β
ββββββββ΄βββββββ
β β
Pragmatica Structural
Lite Core Patterns
JBCT:
- Works within any architectural style (Layered, Hexagonal, Clean)
- Uses railway-oriented error handling
- Is simpler than full FP libraries
- Provides structural patterns other approaches lack
- Includes methodology beyond just types
Exercises
-
Map your architecture: Does your current project use Layered, Hexagonal, or Clean Architecture? Where would JBCT patterns fit?
-
Compare error handling: Take one exception-based method in your codebase. Rewrite it using ROP style with Result. What errors were implicit?
-
Evaluate vavr: If you use vavr, identify which features you actually use. Could you replace it with Pragmatica Core?
-
Cross-language patterns: If you have Kotlin services, compare how Arrow-ktβs Either compares to JBCTβs Result. Are the patterns similar?
Summary
JBCT is not revolutionary - it combines proven ideas:
| Idea | Source |
|---|---|
| Ports and Adapters | Hexagonal Architecture |
| Use Cases as first-class | Clean Architecture |
| Errors as values | Railway-Oriented Programming |
| Functional types | vavr, Arrow-kt, Haskell |
| Parse donβt validate | Type-driven design |
What JBCT adds:
- Unified methodology - Architecture + implementation + testing
- Structural patterns - Named, composable patterns
- Team practices - Migration path, code review guidelines
- AI optimization - Predictable code for AI collaboration
The goal isnβt originality - itβs unification.