This repository contains the complete demo project for the talk "Own Your Design: Functional Principles vs Framework-Driven Architecture", showcasing how to evolve from framework-driven layered architecture to functional hexagonal architecture.
πΊ Watch the talk | π View slides
This talk explores how developers can better control complexity by owning their design choices instead of allowing frameworks to dictate architecture. It contrasts:
- Framework-driven development (Spring Boot, annotations, dependency injection magic, implicit runtime behavior)
- Functional & explicit design (domain modeling, typed errors, pure functions, deterministic flow, boundary-aware architecture)
- Frameworks are tools, not architecture β Don't let Spring write your code
- Own your domain model β Rich types, explicit errors, enforced invariants
- Hexagonal architecture β Predictable, testable, and future-proof
- Typed error handling β Robustness through explicit failure modes
- Functional patterns β Regain control over complexity
This repository contains two parallel implementations of the same domain:
Modern Java implementation using:
- Java 25 with sealed interfaces, pattern matching, records
- Spring Boot 3.5.6 for infrastructure
- JUnit 5 + Mockito for testing
- ArchUnit 1.4.1 for architecture validation
- Explicit Result types for error handling
Functional Kotlin implementation using:
- Kotlin 2.2.20 with context parameters
- Arrow-kt 2.2.0 for functional programming (Raise, Either, effect handlers)
- Kotest for testing
- Konsist 0.17.3 for architecture validation
- Spring Boot 3.5.6 for infrastructure
- kotlin-logging 7.0.3
Both implementations demonstrate the same architectural evolution using idiomatic patterns for each language.
mainβ Refactored hexagonal architecture solutionlayeredβ Traditional framework-driven layered architecturehexagonalβ Clean hexagonal architecture- PR #2 β Detailed diff showing the refactoring journey
domain/
βββ model/ # Pure domain models (value objects, entities)
β βββ Transaction.kt
β βββ CategorizedTransaction.kt
βββ api/ # Inbound ports (use cases, queries)
β βββ TransactionCategorizer.kt
βββ spi/ # Outbound ports (repository interfaces)
βββ CategorizedTransactionRepository.kt
controller/ # Inbound adapters (REST, messaging)
βββ CategorizedTransactionController.kt
infra/ # Outbound adapters (JPA, Kafka, HTTP)
βββ jpa/ # Database adapters
βββ kafka/ # Event streaming adapters
Key Principles:
- Domain at the center, independent of frameworks
- Ports define contracts (interfaces)
- Adapters implement infrastructure concerns
- Dependencies point inward
Kotlin with Arrow:
context(Raise<DomainError>)
fun categorizeTransaction(transaction: Transaction): CategorizedTransaction {
ensure(transaction.amount.isPositive()) { InvalidAmount }
val category = merchantDirectory.findCategory(transaction.merchant)
.bind() // Short-circuits on error
// ...
}Java 25 with Sealed Interfaces:
sealed interface Result<T, E> permits Success, Failure {}
Result<CategorizedTransaction, DomainError> categorize(Transaction tx) {
if (!tx.amount().isPositive()) {
return new Failure<>(new InvalidAmount());
}
// ...
}- Value objects with enforced invariants
- Make illegal states unrepresentable
- Rich domain types over primitives
- No framework dependencies
- Domain depends on nothing
- Application depends only on domain
- Infrastructure depends on domain interfaces
- Framework used only at boundaries
Kotlin Coroutines:
parZip(
{ fetchMerchantInfo(merchantId) },
{ checkBudget(categoryId) }
) { merchant, budget -> /* combine */ }Java Virtual Threads:
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
var merchantTask = scope.fork(() -> fetchMerchant(id));
var budgetTask = scope.fork(() -> checkBudget(categoryId));
scope.join().throwIfFailed();
// ...
}- JDK 25 (for latest Java features)
- Gradle 8.x
- Docker (for Testcontainers in tests)
Java implementation:
./gradlew :fvf4j:build
./gradlew :fvf4j:testKotlin implementation:
./gradlew :fvf4k:build
./gradlew :fvf4k:testArchitecture validation:
# Java - ArchUnit tests
./gradlew :fvf4j:test --tests "*ArchTest"
# Kotlin - Konsist tests
./gradlew :fvf4k:test --tests "*ArchitectureTest"Pure unit tests with no Spring context:
@Test
fun `should categorize transaction with known merchant`() {
val transaction = Transaction(/*...*/)
val result = categorizer.categorize(transaction)
// Assert on domain behavior
}Enforce architectural rules automatically:
// Konsist (Kotlin)
@Test
fun `domain layer should not depend on Spring`() {
Konsist.scopeFromProject()
.classes()
.withPackage("..domain..")
.shouldNot {
it.hasAnnotationOf<RestController>() ||
it.hasAnnotationOf<Service>()
}
}// ArchUnit (Java)
@Test
void domainShouldNotDependOnSpring() {
noClasses()
.that().resideInAPackage("..domain..")
.should().dependOnClassesThat()
.resideInAPackage("org.springframework..")
.check(classes);
}Use Testcontainers for real infrastructure:
- PostgreSQL for database
- Kafka for messaging
- WireMock for HTTP clients
The demo implements a transaction categorization system:
- Input: Raw financial transaction arrives via Kafka
- Categorize: Apply business rules using merchant directory
- Validate: Check against category budgets
- Store: Persist categorized transaction
- Query: Expose REST endpoints for retrieval
Demonstrates:
- Event-driven architecture (Kafka listener adapter)
- Domain services with business rules
- Typed error handling throughout the flow
- Read/write repository separation (CQRS-lite)
- REST API adapter
See PROMPTS.md and the Markdown prompts under prompts/ for detailed guidance on:
- Architecture patterns and conventions
- Code style requirements (Kotlin/Java)
- Testing strategies
- Common refactoring scenarios
- Request templates for new features
| Technology | Java Version | Kotlin Version |
|---|---|---|
| Language | Java 25 | Kotlin 2.2.20 |
| Spring Boot | 3.5.6 | 3.5.6 |
| Testing | JUnit 5, Mockito 5.20.0 | Kotest |
| Arch Tests | ArchUnit 1.4.1 | Konsist 0.17.3 |
| FP Library | Sealed interfaces + records | Arrow-kt 2.2.0 |
| Database | PostgreSQL 42.7.5 | PostgreSQL 42.7.5 |
| Messaging | Spring Kafka 3.2.2 | Spring Kafka |
- Framework annotations in domain layer
- Exceptions for control flow in domain
- Anemic domain models (just getters/setters)
- Direct database access from controllers
- Business logic in controllers or repositories
- Pure functions where possible
- Explicit error types
- Value objects with validation
- Dependency inversion (ports/adapters)
- Deterministic, testable code
- Rich domain model
To fully understand the motivation and design decisions, we recommend:
- Watch the full talk (conference presentation)
- Review the slides (linked in talk description)
- Compare branches: layered β hexagonal β main
- Read the PR diff to see the refactoring steps
This is a demo project for educational purposes. Feel free to:
- Open issues for questions or discussions
- Submit PRs to improve examples
- Share your own refactoring experiences
[Specify your license here]
Remember: Frameworks are accelerators, not architecture. Own your design.