Skip to content

tibtof/fun-vs-framework

Repository files navigation

Gradle Build

Own Your Design: Functional Principles vs Framework-Driven Architecture

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

🎯 Talk Summary

This talk explores how developers can better control complexity by owning their design choices instead of allowing frameworks to dictate architecture. It contrasts:

  1. Framework-driven development (Spring Boot, annotations, dependency injection magic, implicit runtime behavior)
  2. Functional & explicit design (domain modeling, typed errors, pure functions, deterministic flow, boundary-aware architecture)

Core Messages

  • 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

πŸ“ Project Structure

This repository contains two parallel implementations of the same domain:

/fvf4j β€” Java 25 Implementation

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

/fvf4k β€” Kotlin 2.2 Implementation

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.

πŸ—οΈ Architecture Comparison

Branch Structure

  • main β€” Refactored hexagonal architecture solution
  • layered β€” Traditional framework-driven layered architecture
  • hexagonal β€” Clean hexagonal architecture
  • PR #2 β€” Detailed diff showing the refactoring journey

Hexagonal Architecture (Ports & Adapters)

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

πŸ”‘ Key Concepts Demonstrated

1. Typed Error Handling

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());
    }
    // ...
}

2. Domain Modeling

  • Value objects with enforced invariants
  • Make illegal states unrepresentable
  • Rich domain types over primitives
  • No framework dependencies

3. Dependency Inversion

  • Domain depends on nothing
  • Application depends only on domain
  • Infrastructure depends on domain interfaces
  • Framework used only at boundaries

4. Concurrency Design

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();
    // ...
}

πŸš€ Getting Started

Prerequisites

  • JDK 25 (for latest Java features)
  • Gradle 8.x
  • Docker (for Testcontainers in tests)

Build & Test

Java implementation:

./gradlew :fvf4j:build
./gradlew :fvf4j:test

Kotlin implementation:

./gradlew :fvf4k:build
./gradlew :fvf4k:test

Architecture validation:

# Java - ArchUnit tests
./gradlew :fvf4j:test --tests "*ArchTest"

# Kotlin - Konsist tests
./gradlew :fvf4k:test --tests "*ArchitectureTest"

πŸ§ͺ Testing Strategy

Domain Tests

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
}

Architecture Tests

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);
}

Integration Tests

Use Testcontainers for real infrastructure:

  • PostgreSQL for database
  • Kafka for messaging
  • WireMock for HTTP clients

πŸ“š Domain Example: Transaction Categorization

The demo implements a transaction categorization system:

  1. Input: Raw financial transaction arrives via Kafka
  2. Categorize: Apply business rules using merchant directory
  3. Validate: Check against category budgets
  4. Store: Persist categorized transaction
  5. 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

πŸŽ“ Learning Resources

For AI Assistants

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

Key Dependencies

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

πŸ› οΈ Development Guidelines

What to Avoid ❌

  • 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

What to Embrace βœ…

  • Pure functions where possible
  • Explicit error types
  • Value objects with validation
  • Dependency inversion (ports/adapters)
  • Deterministic, testable code
  • Rich domain model

πŸ“– Additional Context

To fully understand the motivation and design decisions, we recommend:

  1. Watch the full talk (conference presentation)
  2. Review the slides (linked in talk description)
  3. Compare branches: layered β†’ hexagonal β†’ main
  4. Read the PR diff to see the refactoring steps

🀝 Contributing

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

πŸ“„ License

[Specify your license here]


Remember: Frameworks are accelerators, not architecture. Own your design.

About

Demo project for my talk Own Your Design: Functional Principles vs the Framework

Resources

License

Stars

Watchers

Forks

Contributors