Module 12: Capstone Project¶
← Module 11: Testing in Kotlin | Topic Home
Build a full-stack Kotlin project: a Spring Boot REST API, an Android app, and a shared Kotlin Multiplatform model library — all tested and documented.
Table of Contents¶
- Overview
- Prerequisites
- Project Brief
- Architecture
- Milestones
- Acceptance Criteria
- Getting Unstuck
- Submission and Review
- Cross-Links
Overview¶
This is the capstone module. You will not be reading theory here — you will be building a real, non-trivial application that synthesizes everything from Modules 01–11.
The project is a personal bookmark manager: users can save URLs with titles, tags, and notes; the Android app provides the mobile interface; the Spring Boot API provides the backend with persistence; a shared Kotlin Multiplatform library contains the domain models and validation logic used by both.
[!IMPORTANT] Do not look for a complete solution. The help sections below provide staged hints, architecture suggestions, and links back to relevant modules — but they are designed for a stuck learner to move forward on their own, not to hand you the answer. A capstone you built yourself is worth infinitely more than one you copy-pasted.
If you're stuck, read the relevant help section, try to implement it, and come back to the next hint only if you're still blocked after a genuine attempt.
Prerequisites¶
All 11 prior modules, with particular emphasis on: - Module 03 (Kotlin coroutines) - Module 04–07 (Spring Boot) - Module 08 (Gradle multi-module) - Module 09–10 (Android) - Module 11 (Testing)
Project Brief¶
Application: KotlinMark — Personal Bookmark Manager
Build a system with three components:
Component 1: shared — Kotlin Multiplatform Library¶
A KMP module (targets JVM and Android) containing:
- Domain models: Bookmark, Tag, User
- Validation logic: URL validation, title length limits, tag name rules
- Serialization-ready data classes (with kotlinx.serialization)
Component 2: backend — Spring Boot REST API¶
A Spring Boot application that:
- Exposes a REST API for bookmarks CRUD
- Requires JWT authentication
- Persists data to PostgreSQL (via Spring Data JPA)
- Uses the shared KMP library's domain models
- Has comprehensive tests (unit + integration with TestContainers)
- Exposes Actuator health endpoint
Component 3: android — Android App¶
An Android app that:
- Uses Jetpack Compose for UI with Material3 design
- Authenticates against the backend API (login screen → JWT stored securely)
- Lists, creates, updates, and deletes bookmarks
- Supports offline mode: shows cached bookmarks when offline (Room)
- Uses the shared KMP library's models
- Has at least 5 meaningful UI test cases
Architecture¶
kotlin-mark/
├── settings.gradle.kts ← includes all three subprojects
├── gradle/
│ └── libs.versions.toml ← shared dependency versions
├── shared/
│ ├── build.gradle.kts ← kotlin("multiplatform") plugin
│ └── src/
│ ├── commonMain/kotlin/ ← domain models, validation
│ ├── jvmMain/kotlin/ ← JVM-specific implementations (optional)
│ └── androidMain/kotlin/ ← Android-specific implementations (optional)
├── backend/
│ ├── build.gradle.kts ← Spring Boot + Kotlin + JPA
│ └── src/
│ ├── main/kotlin/
│ └── test/kotlin/
└── android/
├── build.gradle.kts ← Android application plugin
└── src/
├── main/kotlin/
└── androidTest/kotlin/
Milestones¶
Complete milestones in order. Each builds on the previous.
Milestone 1: Project Setup and Shared Module¶
- Create the three-module Gradle project structure
- Configure the Kotlin Multiplatform plugin in the
sharedmodule - Define
Bookmark,Tag, andUserdata classes incommonMain - Add validation functions for each domain model
- Write unit tests for all validators
Done when: ./gradlew :shared:test passes.
Milestone 2: Spring Boot API — Core¶
- Add the
sharedmodule as a dependency ofbackend - Create JPA entities for Bookmark and User (note: JPA entities are not the same as domain models — you will need mappers)
- Implement CRUD endpoints for bookmarks:
GET /bookmarks,POST /bookmarks,GET /bookmarks/{id},PUT /bookmarks/{id},DELETE /bookmarks/{id} - Add JWT authentication: register, login, refresh
- Add pagination:
GET /bookmarks?page=0&size=20
Done when: All endpoints respond correctly in Postman/curl, authenticated correctly.
Milestone 3: Spring Boot API — Tests and Quality¶
- Add
@WebMvcTesttests for all controller endpoints - Add
@DataJpaTesttests for repository queries - Add TestContainers integration test for the full bookmark creation flow (create bookmark → retrieve it → verify stored correctly)
- Configure Actuator health endpoint
- Add
docker-compose.ymlto run the API + PostgreSQL locally with one command
Done when: ./gradlew :backend:test passes; docker-compose up starts the working system.
Milestone 4: Android App — Core¶
- Add the
sharedmodule as a dependency ofandroid - Implement authentication screens: Login and Register with proper error handling
- Implement bookmark list screen:
LazyColumnwithBookmarkCardcomposables - Implement create/edit bookmark screen
- Wire Retrofit to call the Spring Boot API
- Store JWT securely (
EncryptedSharedPreferences)
Done when: The Android app authenticates against the running backend and shows real bookmarks.
Milestone 5: Android App — Offline and Tests¶
- Implement Room for local bookmark caching
- Implement offline-first: show Room data immediately, refresh from API in background, update Room on success
- Write 5 Compose UI tests using
createComposeRule() - Handle all error states gracefully: network errors, auth errors, empty state
Done when: The app shows bookmarks when offline. ./gradlew :android:connectedAndroidTest passes (or passes on emulator).
Milestone 6: Polish and Documentation¶
- README.md in the project root with setup instructions, architecture diagram, and screenshots
- All three modules build cleanly with no warnings
- Code is formatted with
ktlintordetekt - At least one non-trivial feature not in the brief (your choice: search, tags filter, share sheet, bookmark import from browser, etc.)
Done when: A colleague can clone the repo and get everything running by following the README alone.
Acceptance Criteria¶
Your capstone is complete when:
-
./gradlew buildpasses for all three modules - The backend API has ≥70% test coverage (JaCoCo)
- All TestContainers tests pass
- The Android app runs on a real device or emulator, connected to the backend
- The shared module compiles for both JVM and Android targets
- The project README documents architecture, setup, and design decisions
Getting Unstuck¶
Hint: KMP module setup is failing
The Kotlin Multiplatform plugin requires the `kotlin("multiplatform")` plugin (not `kotlin("jvm")`). Your `shared/build.gradle.kts` should look like: See Module 08 for Gradle Kotlin DSL patterns.Hint: JPA entities vs KMP domain models
You cannot use KMP `data class` directly as a JPA entity because: 1. JPA requires no-args constructors on mutable fields 2. KMP `commonMain` code cannot use JPA annotations (they're JVM-only) The solution is a mapper pattern: - `shared/commonMain` has `Bookmark(val id: UUID, val url: String, val title: String)` — pure domain model - `backend` has `BookmarkEntity(@Entity class BookmarkEntity(...))` — JPA entity - A mapper function converts between them: `fun Bookmark.toEntity(): BookmarkEntity` and `fun BookmarkEntity.toDomain(): Bookmark` See Module 05 for JPA entity patterns.Hint: JWT token storage on Android
Never store JWTs in SharedPreferences in plaintext — they can be extracted on rooted devices. Use `EncryptedSharedPreferences` from the Security library:val masterKey = MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build()
val prefs = EncryptedSharedPreferences.create(
context, "secure_prefs", masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
prefs.edit().putString("access_token", token).apply()
Hint: Offline-first with Room + Retrofit
The standard pattern for offline-first in Android:fun getBookmarks(): Flow<List<Bookmark>> = flow {
// Step 1: emit cached data from Room immediately
emitAll(bookmarkDao.observeAll().map { entities -> entities.map { it.toDomain() } })
}.also {
// Step 2: fetch from network in parallel and update Room
viewModelScope.launch {
try {
val fresh = api.getBookmarks()
bookmarkDao.upsertAll(fresh.map { it.toEntity() })
// Room Flow will automatically emit the updated data
} catch (e: Exception) {
// Network failed — cached data is already showing; log and continue
}
}
}
Hint: TestContainers PostgreSQL configuration
Use `@DynamicPropertySource` to point Spring Boot at the TestContainers database:companion object {
@Container @JvmStatic
val postgres = PostgreSQLContainer<Nothing>("postgres:16-alpine").apply {
withDatabaseName("kotlinmark_test")
}
@DynamicPropertySource @JvmStatic
fun props(registry: DynamicPropertyRegistry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl)
registry.add("spring.datasource.username", postgres::getUsername)
registry.add("spring.datasource.password", postgres::getPassword)
registry.add("spring.jpa.hibernate.ddl-auto") { "create-drop" }
}
}
Submission and Review¶
When you complete the capstone:
- Push your code to a GitHub repository (public or private)
- Update
TOPICS/kotlin-spring-android/PROGRESS.mdwith your project link - Append a journal entry to
TOPICS/kotlin-spring-android/README.mdwith what you built and what you learned - Ask for a code review by creating a question in
QUESTIONS.mdwith a link to your repo
Cross-Links¶
- [[kotlin-spring-android/modules/03_kotlin-advanced]] — Coroutines used throughout
- [[kotlin-spring-android/modules/04_spring-boot-fundamentals]] — API architecture
- [[kotlin-spring-android/modules/05_spring-data-and-persistence]] — JPA entities and repositories
- [[kotlin-spring-android/modules/06_spring-security]] — JWT authentication
- [[kotlin-spring-android/modules/08_gradle-and-build]] — Multi-module Gradle structure
- [[kotlin-spring-android/modules/09_android-fundamentals]] — Compose UI
- [[kotlin-spring-android/modules/10_android-advanced]] — ViewModel, Room, Retrofit, Hilt
- [[kotlin-spring-android/modules/11_testing-kotlin]] — Tests across all three modules
- [[postgresql]] — PostgreSQL used in the backend