Version: 1.0.0
Date: September 29, 2025
License: BSD-3-Clause
Copyright: © 2025 Michael Gardner, A Bit of Help, Inc.
Authors: Michael Gardner
Status: Released
A production-ready Kotlin template implementing a hybrid of Domain-Driven Design (DDD), Clean Architecture, and Hexagonal Architecture principles. This template provides a solid foundation for building maintainable, testable, and scalable applications.
- Quick Start
- Architecture Overview
- Project Structure
- Getting Started
- Development Workflow
- Testing Strategy
- Common Patterns
- Troubleshooting
- Contributing
# Clone the repository
git clone https://github.com/yourusername/kotlin-hybrid-architecture-template.git
cd kotlin-hybrid-architecture-template
# Build the project
./gradlew build
# Run tests
./gradlew test
# Run the application
./gradlew :bootstrap:run --args="YourName"
# Additional CLI options
./gradlew :bootstrap:run --args="--help" # Show help
./gradlew :bootstrap:run --args="--version" # Show version
./gradlew :bootstrap:run --args="--quiet YourName" # Quiet mode
./gradlew :bootstrap:run --args="--verbose YourName" # Verbose output
./gradlew :bootstrap:run --args="--out=output.txt YourName" # File output
This template combines the best aspects of three proven architectural patterns:
Traditional architectures often force you to choose between different approaches. This template recognizes that each pattern has strengths:
- Domain-Driven Design (DDD): Focuses on modeling your business domain accurately
- Clean Architecture: Ensures dependencies point inward toward business logic
- Hexagonal Architecture: Isolates your application from external concerns through ports and adapters
- Dependency Rule: Dependencies only point inward toward the domain layer
- Isolation of Concerns: Each layer has a specific responsibility
- Testability: Every component can be tested in isolation
- Flexibility: Easy to swap implementations without affecting business logic
┌─────────────────────────────────────────────┐
│ Presentation Layer │
│ (CLI, REST Controllers, GraphQL) │
├─────────────────────────────────────────────┤
│ Application Layer │
│ (Use Cases, Application Services) │
├─────────────────────────────────────────────┤
│ Domain Layer │
│ (Entities, Value Objects, Domain Services) │
├─────────────────────────────────────────────┤
│ Infrastructure Layer │
│ (Database, File System, External APIs) │
└─────────────────────────────────────────────┘
kotlin-hybrid-architecture-template/
├── domain/ # Core business logic (no dependencies)
│ └── src/main/kotlin/
│ ├── model/ # Entities and aggregates
│ ├── value/ # Value objects
│ ├── service/ # Domain services
│ └── error/ # Domain-specific errors
│
├── application/ # Use cases and orchestration
│ └── src/main/kotlin/
│ ├── usecase/ # Business use cases
│ ├── port/ # Interface definitions
│ │ ├── input/ # Incoming ports (use cases)
│ │ └── output/ # Outgoing ports (repositories)
│ └── dto/ # Data transfer objects
│
├── infrastructure/ # External implementations
│ └── src/main/kotlin/
│ └── adapter/ # Port implementations
│ ├── output/ # File, database adapters
│ └── input/ # REST, CLI adapters
│
├── presentation/ # User interfaces
│ └── src/main/kotlin/
│ └── cli/ # Command-line interface
│
├── bootstrap/ # Application startup
│ └── src/main/kotlin/
│ └── config/ # Dependency injection
│
└── architecture-tests/ # Architecture verification
Before you begin, ensure you have:
- JDK 21 or higher: The project uses modern Java features
- Gradle: Build automation (wrapper included)
- Git: Version control
-
Clone and Navigate
git clone <repository-url> cd kotlin-hybrid-architecture-template
-
Verify Setup
./gradlew --version java -version
-
Build the Project
./gradlew clean build
Let's trace a simple request through the architecture:
- User Input: User runs
./gradlew :bootstrap:run --args="Alice"
(or with options like--verbose
,--quiet
,--version
) - Bootstrap: The bootstrap module starts the application
- Presentation: CLI parses the command and creates a request
- Application: Use case orchestrates the business logic
- Domain: Domain service creates a greeting
- Infrastructure: Output adapter writes to console/file
- Response: User sees "Hello, Alice!"
Let's add a new feature - personalizing greetings based on time of day:
-
Start with the Domain (domain/src/main/kotlin/...)
// value/TimeOfDay.kt enum class TimeOfDay { MORNING, AFTERNOON, EVENING, NIGHT } // service/TimeAwareGreetingService.kt interface TimeAwareGreetingService { fun greetWithTime(name: String, timeOfDay: TimeOfDay): String }
-
Implement in Application (application/src/main/kotlin/...)
// usecase/CreateTimeAwareGreetingUseCase.kt class CreateTimeAwareGreetingUseCase( private val greetingService: TimeAwareGreetingService, private val outputPort: OutputPort ) : CreateTimeAwareGreetingInputPort { // Implementation }
-
Add Infrastructure Support (infrastructure/src/main/kotlin/...)
// adapter/service/DefaultTimeAwareGreetingService.kt class DefaultTimeAwareGreetingService : TimeAwareGreetingService { override fun greetWithTime(name: String, timeOfDay: TimeOfDay): String { return when (timeOfDay) { MORNING -> "Good morning, $name!" AFTERNOON -> "Good afternoon, $name!" EVENING -> "Good evening, $name!" NIGHT -> "Good night, $name!" } } }
-
Always Start with Domain
- Define your business concepts
- Create value objects for type safety
- Design interfaces for services
- No external dependencies allowed!
-
Move to Application Layer
- Create use cases for each business operation
- Define ports (interfaces) for external needs
- Keep orchestration logic here
-
Implement Infrastructure
- Create adapters for your ports
- Handle technical concerns (files, network, etc.)
- Keep framework-specific code here
-
Connect via Bootstrap
- Wire dependencies in CompositionRoot
- Configure application settings
- Handle startup/shutdown
Run these commands regularly:
# Format code
./gradlew ktlintFormat
# Check code style
./gradlew ktlintCheck
# Run static analysis
./gradlew detekt
# Run all checks
./gradlew check
Adding a New Use Case:
- Define the interface in
application/port/input/
- Implement in
application/usecase/
- Add tests in
application/src/test/kotlin/
- Wire in
bootstrap/CompositionRoot.kt
Adding a New Output Adapter:
- Define port in
application/port/output/
- Implement in
infrastructure/adapter/output/
- Add integration tests
- Update CompositionRoot
╱─────╲
╱ E2E ╲ Few tests, high confidence
╱─────────╲
╱Integration╲ Test adapters and integration
╱─────────────╲
╱ Unit Tests ╲ Many tests, fast feedback
╱─────────────────╲
Domain Tests - Pure logic, no mocks needed:
class GreetingTest : DescribeSpec({
describe("Greeting creation") {
it("should create formal greeting") {
val greeting = Greeting.formal("Alice")
greeting.message shouldBe "Good day, Ms. Alice"
}
}
})
Use Case Tests - Mock external dependencies:
class CreateGreetingUseCaseTest : DescribeSpec({
describe("CreateGreetingUseCase") {
val mockOutput = mockk<OutputPort>()
val useCase = CreateGreetingUseCase(mockOutput)
it("should send greeting to output") {
every { mockOutput.send(any()) } returns Unit.right()
val result = useCase.execute(CreateGreetingCommand("Alice"))
result.shouldBeRight()
verify { mockOutput.send("Hello, Alice!") }
}
}
})
Architecture Tests - Verify architectural rules:
class ArchitectureTest : DescribeSpec({
describe("Architecture Rules") {
it("domain should not depend on other layers") {
classes()
.that().resideInPackage("..domain..")
.should().onlyDependOnClassesThat()
.resideInPackages("..domain..", "java..", "kotlin..")
.check(allClasses)
}
}
})
Instead of throwing exceptions, use Either for explicit error handling:
// Don't do this
fun riskyOperation(): String {
if (somethingBad) throw Exception("Failed!")
return "Success"
}
// Do this
fun safeOperation(): Either<DomainError, String> {
return if (somethingBad) {
DomainError.ValidationError("Failed!").left()
} else {
"Success".right()
}
}
// Usage
when (val result = safeOperation()) {
is Either.Left -> handleError(result.value)
is Either.Right -> processSuccess(result.value)
}
The bootstrap module uses manual dependency injection:
object CompositionRoot {
fun buildApplication(config: AppConfig): Application {
// Create infrastructure
val repository = FileRepository(config.dataPath)
// Create domain services
val domainService = DefaultDomainService()
// Create use cases
val useCase = CreateEntityUseCase(repository, domainService)
// Create presentation
return CliApplication(useCase)
}
}
Don't use primitive types for domain concepts:
// Don't do this
fun transfer(fromAccount: String, toAccount: String, amount: Double)
// Do this
fun transfer(from: AccountId, to: AccountId, amount: Money)
// Define value objects
@JvmInline
value class AccountId(val value: String) {
init {
require(value.matches(Regex("[A-Z]{2}\\d{8}"))) {
"Invalid account ID format"
}
}
}
data class Money(
val amount: BigDecimal,
val currency: Currency
) {
init {
require(amount >= BigDecimal.ZERO) {
"Amount cannot be negative"
}
}
}
Issue: "Cannot find symbol" compilation errors
- Cause: Missing imports or incorrect package structure
- Solution: Ensure all imports are present and packages match directory structure
- Prevention: Use IDE auto-import features
Issue: Circular dependency detected
- Cause: Two modules depending on each other
- Solution: Extract common interfaces to a shared module
- Prevention: Follow the dependency rule strictly
Issue: Tests fail with "No value present"
- Cause: Trying to access empty Optional/Either without checking
- Solution: Always check before accessing:
either.fold({ error -> }, { value -> })
- Prevention: Use sealed classes for exhaustive when expressions
Issue: OutOfMemoryError during build
- Cause: Insufficient heap space for Gradle
- Solution: Add to
gradle.properties
:org.gradle.jvmargs=-Xmx2g
- Prevention: Close unnecessary applications during build
-
Enable Detailed Logging
./gradlew test --info --stacktrace
-
Run Single Test
./gradlew test --tests "com.example.MyTest.my test method"
-
Check Dependencies
./gradlew :domain:dependencies
If you encounter performance issues:
- Profile First: Use JVM profilers to identify bottlenecks
- Optimize Carefully: Don't optimize prematurely
- Consider Caching: Add caching at infrastructure layer
- Use Coroutines: For I/O-bound operations
- Read the architecture guides in
/docs/guides/
- Run all tests:
./gradlew test
- Check code style:
./gradlew ktlintCheck detekt
- Update documentation for significant changes
- Create a feature branch
- Make your changes following the architecture
- Add tests for new functionality
- Ensure all checks pass
- Submit a pull request
- Follows dependency rule (no outward dependencies)
- Includes appropriate tests
- Documentation updated
- No code smells detected by tools
- Follows Kotlin conventions
- Explore the Guides: Read
/docs/guides/
for detailed explanations - Run the Examples: Try the sample application
- Experiment: Create a new use case
- Learn More: Study the architecture tests
- Domain-Driven Design by Eric Evans
- Clean Architecture by Robert C. Martin
- Hexagonal Architecture by Alistair Cockburn
- Kotlin Official Documentation
This project is licensed under the BSD-3-Clause License. See the LICENSE file for details.