Kotlin Project Structure
When you're starting with Kotlin development, understanding how to structure your projects properly is crucial for maintainability, scalability, and collaboration. A well-organized project structure makes your code easier to navigate, test, and extend.
Introduction
Project structure refers to how you organize your source code, resources, configuration files, and dependencies. In Kotlin projects, the structure is typically influenced by the build system (usually Gradle or Maven) and follows conventions that make it easy for any developer to understand the codebase.
In this guide, we'll explore:
- Standard Kotlin project structure
- Setting up build files
- Package organization strategies
- Module separation
- Resource management
- Best practices for maintainable project organization
Standard Kotlin Project Structure
A typical Kotlin project follows this basic structure:
project-root/
├── build.gradle.kts (or build.gradle)
├── settings.gradle.kts (or settings.gradle)
├── gradle/
│ └── wrapper/
├── gradlew
├── gradlew.bat
├── src/
│ ├── main/
│ │ ├── kotlin/
│ │ │ └── com/example/project/
│ │ │ ├── models/
│ │ │ ├── services/
│ │ │ └── App.kt
│ │ └── resources/
│ └── test/
│ ├── kotlin/
│ │ └── com/example/project/
│ └── resources/
└── README.md
Let's break down what each part does:
- build.gradle.kts: The primary build configuration file using Kotlin DSL for Gradle
- settings.gradle.kts: Defines project settings, including module inclusion
- gradle/wrapper/: Contains Gradle Wrapper files for consistent build environment
- gradlew & gradlew.bat: Gradle Wrapper executables for Unix and Windows
- src/main/kotlin/: Your main source code files go here
- src/main/resources/: Configuration files, images, and other non-code assets
- src/test/: Contains test code and test-specific resources
Setting Up the Build File
Your build.gradle.kts
file is the central configuration for your project. Here's an example of a basic Kotlin project build file:
plugins {
kotlin("jvm") version "1.9.0"
application
}
group = "com.example"
version = "1.0-SNAPSHOT"
repositories {
mavenCentral()
}
dependencies {
implementation(kotlin("stdlib"))
// Testing dependencies
testImplementation(kotlin("test"))
testImplementation("org.junit.jupiter:junit-jupiter:5.9.3")
}
application {
mainClass.set("com.example.project.AppKt")
}
tasks.test {
useJUnitPlatform()
}
This build script:
- Applies the Kotlin JVM plugin
- Sets group and version information
- Configures Maven Central as a dependency source
- Adds Kotlin standard library and testing dependencies
- Configures the main class for the application
- Sets up JUnit for testing
Package Organization Strategies
Organizing packages in your Kotlin project is crucial for maintainability. Here are some common approaches:
By Feature
com.example.project/
├── feature1/
│ ├── Feature1Service.kt
│ ├── Feature1Repository.kt
│ └── models/
├── feature2/
│ ├── Feature2Service.kt
│ └── models/
└── common/
└── utils/
This approach groups code by features, making it easy to understand which files are related to specific functionality.
By Layer
com.example.project/
├── controllers/
├── services/
├── repositories/
├── models/
└── utils/
This traditional approach separates code by its architectural layer.
Example: Creating a Basic Kotlin Project Structure
Let's implement a simple todo application with proper project structure:
1. Set up the directory structure
todo-app/
├── build.gradle.kts
├── settings.gradle.kts
└── src/
├── main/
│ ├── kotlin/
│ │ └── com/example/todo/
│ │ ├── models/
│ │ │ └── Task.kt
│ │ ├── repositories/
│ │ │ └── TaskRepository.kt
│ │ ├── services/
│ │ │ └── TaskService.kt
│ │ └── App.kt
│ └── resources/
│ └── config.properties
└── test/
└── kotlin/
└── com/example/todo/
└── services/
└── TaskServiceTest.kt
2. Create the model class
// src/main/kotlin/com/example/todo/models/Task.kt
package com.example.todo.models
data class Task(
val id: Int,
val title: String,
val description: String,
val isCompleted: Boolean = false
)
3. Implement the repository
// src/main/kotlin/com/example/todo/repositories/TaskRepository.kt
package com.example.todo.repositories
import com.example.todo.models.Task
class TaskRepository {
private val tasks = mutableListOf<Task>()
fun add(task: Task) {
tasks.add(task)
}
fun getAll(): List<Task> {
return tasks.toList()
}
fun getById(id: Int): Task? {
return tasks.find { it.id == id }
}
fun update(task: Task) {
val index = tasks.indexOfFirst { it.id == task.id }
if (index >= 0) {
tasks[index] = task
}
}
fun delete(id: Int) {
tasks.removeIf { it.id == id }
}
}
4. Create the service layer
// src/main/kotlin/com/example/todo/services/TaskService.kt
package com.example.todo.services
import com.example.todo.models.Task
import com.example.todo.repositories.TaskRepository
class TaskService(private val repository: TaskRepository) {
fun createTask(title: String, description: String): Task {
val newId = repository.getAll().maxOfOrNull { it.id }?.plus(1) ?: 1
val task = Task(newId, title, description)
repository.add(task)
return task
}
fun getAllTasks(): List<Task> {
return repository.getAll()
}
fun completeTask(id: Int): Task? {
val task = repository.getById(id) ?: return null
val updatedTask = task.copy(isCompleted = true)
repository.update(updatedTask)
return updatedTask
}
fun deleteTask(id: Int) {
repository.delete(id)
}
}
5. Implement the main application
// src/main/kotlin/com/example/todo/App.kt
package com.example.todo
import com.example.todo.repositories.TaskRepository
import com.example.todo.services.TaskService
fun main() {
val repository = TaskRepository()
val service = TaskService(repository)
println("Todo Application")
println("---------------")
// Creating some tasks
service.createTask("Learn Kotlin", "Study Kotlin language features")
service.createTask("Understand project structure", "Learn about proper Kotlin project organization")
service.createTask("Build a project", "Create a sample application with proper structure")
// Completing a task
service.completeTask(1)
// Displaying all tasks
println("\nAll Tasks:")
service.getAllTasks().forEach { task ->
val status = if (task.isCompleted) "✓" else "□"
println("[$status] ${task.id}. ${task.title} - ${task.description}")
}
}
6. Write a test for the service
// src/test/kotlin/com/example/todo/services/TaskServiceTest.kt
package com.example.todo.services
import com.example.todo.repositories.TaskRepository
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class TaskServiceTest {
@Test
fun `should create task with correct title and description`() {
// Arrange
val repository = TaskRepository()
val service = TaskService(repository)
// Act
val task = service.createTask("Test Task", "Test Description")
// Assert
assertEquals("Test Task", task.title)
assertEquals("Test Description", task.description)
assertFalse(task.isCompleted)
}
@Test
fun `should mark task as completed`() {
// Arrange
val repository = TaskRepository()
val service = TaskService(repository)
val task = service.createTask("Test Task", "Test Description")
// Act
val completedTask = service.completeTask(task.id)
// Assert
assertTrue(completedTask!!.isCompleted)
}
}
7. Configure the build file
// build.gradle.kts
plugins {
kotlin("jvm") version "1.9.0"
application
}
group = "com.example.todo"
version = "1.0-SNAPSHOT"
repositories {
mavenCentral()
}
dependencies {
implementation(kotlin("stdlib"))
testImplementation(kotlin("test"))
}
application {
mainClass.set("com.example.todo.AppKt")
}
tasks.test {
useJUnitPlatform()
}
Multi-Module Project Structure
As your project grows, you might want to split it into multiple modules. Here's an example structure for a multi-module project:
todo-app/
├── build.gradle.kts
├── settings.gradle.kts
├── core/
│ ├── build.gradle.kts
│ └── src/
│ └── main/kotlin/
│ └── com/example/todo/core/
├── api/
│ ├── build.gradle.kts
│ └── src/
│ └── main/kotlin/
│ └── com/example/todo/api/
└── app/
├── build.gradle.kts
└── src/
└── main/kotlin/
└── com/example/todo/app/
Your settings.gradle.kts
would include:
rootProject.name = "todo-app"
include("core", "api", "app")
Best Practices for Project Structure
- Keep packages small: Avoid deeply nested package structures.
- Package by feature, not by layer: Group related functionality together.
- Use consistent naming: Adopt a naming convention and stick to it.
- Create clear module boundaries: Each module should have a single responsibility.
- Separate business logic from UI: Keep your domain models independent.
- Organize resources properly: Use resource directories effectively.
- Leverage build configurations: Use build variants for different environments.
Common Pitfalls to Avoid
- Circular dependencies between modules or packages
- Inconsistent naming conventions
- Monolithic classes that don't follow single responsibility principle
- Tight coupling between components
- Misplaced files that don't follow the project structure
Summary
A well-organized Kotlin project structure is essential for maintainable and scalable applications. By following these best practices, you can create projects that are:
- Easy to navigate and understand
- Modular and decoupled
- Simple to test
- Ready for collaboration
- Prepared for growth
Remember that project structure is not just about directories and files—it's about organizing your code in a way that reflects the architecture and domain of your application. Start with a clean structure from the beginning, and it will pay dividends as your project grows.
Additional Resources
- Kotlin Official Documentation
- Gradle Documentation
- Clean Architecture in Kotlin
- Modularization Patterns
Exercises
- Create a new Kotlin project with a proper structure for a library management system.
- Convert an existing single-module project into a multi-module structure.
- Refactor a package-by-layer structure to a package-by-feature organization.
- Add proper test directories and write tests for your domain models.
- Create a Gradle multi-platform project structure that targets both JVM and JS.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)