This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
This is a Kotlin demonstration project showcasing how hexagonal architecture enables testing at multiple layers using the same test contract. The application is a simple bank system with account creation, deposits, and withdrawals.
The project uses hexagonal architecture (ports and adapters) to enforce separation of concerns:
-
domain: Core business logic with no external dependencies
Bankinterface: Primary port (driving) defining all bank operationsBankAccountRepositoryinterface: Secondary port (driven) for data persistenceBankLogic: Implementation ofBankthat orchestrates business rules- Uses
testFixturesto publishBankContract- an abstract test suite used across layers
-
http: HTTP API adapter
BankHttp: ExposesBankinterface via HTTP using http4kBankHttpClient: ImplementsBankinterface by calling the HTTP API- Tests extend
BankContractand test through HTTP endpoints
-
web: Web UI adapter
BankWeb: Handlebars-based web interface- Tests extend
BankContractand test through Selenium WebDriver
The key architectural benefit demonstrated here is contract-based testing:
BankContractindomain/src/testFixturesdefines the complete functional test suite- Each layer extends
BankContractand provides its ownBankimplementation - The same test suite validates behavior at:
- Unit level:
BankLogicTesttests business logic directly - HTTP level:
BankHttpTesttests through the HTTP API - UI level:
BankWebTesttests through the web interface
- Unit level:
This ensures the entire system behaves consistently without duplicating test logic.
# Build and test (preferred - runs tests and verification)
./gradlew check
# Build without tests
./gradlew build
# Run tests for a specific module
./gradlew :domain:test
./gradlew :http:test
./gradlew :web:test
# Run a single test class
./gradlew :domain:test --tests "lmirabal.bank.BankLogicTest"
./gradlew :http:test --tests "lmirabal.bank.http.BankHttpTest"Note: Gradle's incremental build is efficient - running clean is rarely needed and slows down builds. Only use it if you encounter caching issues.
# Run the full web application (domain + HTTP + web UI)
./gradlew :web:runThe application will start on the default http4k port. Access it in a browser to interact with the bank UI.
- Language: Kotlin 2.3.0
- Build: Gradle 9.2.1
- Runtime: JVM 25
- Web Framework: http4k (HTTP API and web templating)
- Testing: JUnit 5, Hamkrest (matchers)
- UI Testing: Selenium WebDriver via http4k-testing-webdriver
- Functional Types: result4k for
Result<T, E>types
The domain uses functional error handling via Result<T, E> from result4k:
withdraw()returnsResult<BankAccount, NotEnoughFunds>instead of throwing exceptions- This allows errors to be mapped to HTTP status codes (400 for insufficient funds) and UI error pages
Amount is stored in minor units (cents) as Long to avoid floating-point precision issues. The web layer converts to/from major units (dollars) for display.
The domain defines BankAccountRepository as an interface. InMemoryBankAccountRepository is the only implementation, but the pattern allows for easy substitution (e.g., database-backed repository).
The domain module uses Gradle's java-test-fixtures plugin to share BankContract with other modules. Other modules declare testImplementation testFixtures(project(':domain')) to access it.
- When adding new
Bankoperations, update bothBankinterface andBankContracttest suite - Each adapter layer (http, web) should implement the new operation and inherit test coverage automatically
- The codebase intentionally minimizes external dependencies to keep the architecture clear
- Pattern matching on
Result<T, E>uses.map()for success path and.recover()for error path