kmp-mongodb-database
>
Works with
---
name: kmp-mongodb-database
description: >
license: Apache-2.0
---
## When to Use This Skill
Use this skill when you need to:
- Build or review a MongoDB-backed backend data layer for a KMP app
- Keep document mapping and repository code cleanly separated
- Use Flow or change streams for reactive reads
- Model typed database failures instead of leaking driver exceptions upward
- Scaffold repeated database setup for a new service
**Recommended default:** keep MongoDB on the server side behind a repository boundary,
map documents to domain models at the edge, and expose typed errors to the caller.
**Trigger keywords:** MongoDB, database, collection, repository, document mapping,
Flow, change stream, server-side Kotlin, coroutine driver, typed errors, backend data.
**Freshness rule:** the MongoDB Kotlin coroutine driver evolves independently of the JVM
driver — recheck the [official docs](https://www.mongodb.com/docs/drivers/kotlin/coroutine/)
before upgrading or scaffolding.
---
## Recommendation First
Default to this pattern:
1. **Server-side MongoDB only** with the official Kotlin coroutine driver.
2. **Repository boundary** between collection access and the rest of the app.
3. **Typed document mapping** at the edge, not in route handlers.
4. **Flow / change streams** for reactive reads when the data should update live.
Why:
- the driver is officially positioned for server-side Kotlin coroutine apps
- repositories keep route handlers and services small
- typed mapping keeps BSON/documents from leaking through the app
---
## Project Structure
Keep the database layer separate from auth and routes:
```text
server/
database/
MongoClientFactory.kt
di/DatabaseModule.kt
user/
data/UserCollection.kt
data/UserDocument.kt
repository/UserRepository.kt
repository/UserRepositoryImpl.kt
```
Rules:
- database bootstrap lives in one place
- collection access is not spread across route handlers
- document mapping happens in the data layer
- repositories expose domain models, not BSON documents
---
## Core Pattern
Use the coroutine driver with a client + database + collection boundary:
```kotlin
class UserRepositoryImpl(
private val collection: MongoCollection<UserDocument>,
) : UserRepository {
override suspend fun findById(id: String): User? =
collection.findOneById(id)?.toDomain()
override fun watchUsers(): Flow<List<User>> =
collection.watch().map { events -> events.map { it.fullDocument.toDomain() } }
}
```
Typed errors stay close to the repository:
```kotlin
sealed interface DatabaseError {
data object NotFound : DatabaseError
data object Unauthorized : DatabaseError
data class Unknown(val message: String) : DatabaseError
}
```
---
## Reactive Reads
Use `Flow` when the UI or service should react to updates:
- live dashboards
- collections that change often
- admin views
- background sync listeners
Do not overuse change streams for simple CRUD if a plain repository call is enough.
---
## Scaffold Script
- `scripts/scaffold_mongodb_database.py` — creates a starter MongoDB data-layer folder
with client, repository, document, and DI files.
---
## Related Skills
- `kmp-ktor-auth-service` — auth service depends on the `UserRepository` built here
- `kmp-kotlin-rpc` — RPC service implementations delegate to MongoDB repositories
- `kmp-repository-pattern` — the same repository interface/implementation pattern applied server-side
- `kmp-dependency-injection` — `MongoClient` and collection bindings are registered as Koin singletons
---
## Testing
MongoDB data layer tests operate at two levels. Both are required.
### Level 1 — Unit tests for services and use cases (FakeRepository)
Services and use cases that depend on `UserRepository` must never use the real
`UserRepositoryImpl` in tests. Use a fake that implements the interface:
```kotlin
// server/user/testing/FakeUserRepository.kt
class FakeUserRepository : UserRepository {
val users = mutableMapOf<String, User>()
var shouldThrow: Exception? = null
override suspend fun findById(id: String): User? {
shouldThrow?.let { throw it }
return users[id]
}
override suspend fun findByEmail(email: String): User? =
users.values.find { it.email == email }
override suspend fun save(user: User): User {
users[user.id] = user
return user
}
override suspend fun delete(id: String): Boolean =
users.remove(id) != null
// Change stream test support — call emitUpdate() from test body
private val _updates = MutableSharedFlow<List<User>>(extraBufferCapacity = 1)
override fun watchUsers(): Flow<List<User>> = _updates
suspend fun emitUpdate(list: List<User>) = _updates.emit(list)
}
```
Example service unit test:
```kotlin
class UserServiceTest {
private val repo = FakeUserRepository()
private val service = UserService(repo)
@Test fun `findById returns user when found`() = runTest {
repo.users["u1"] = User(id = "u1", name = "Alice", email = "alice@example.com")
val result = service.findById("u1")
assertEquals("Alice", result?.name)
}
@Test fun `findById returns null when not found`() = runTest {
assertNull(service.findById("missing"))
}
@Test fun `save persists the user`() = runTest {
val user = User(id = "u2", name = "Bob", email = "bob@example.com")
service.save(user)
assertEquals(user, repo.users["u2"])
}
@Test fun `watchUsers emits on change`() = runTest {
val users = listOf(User("u1", "Alice", "alice@example.com"))
service.watchUsers().test {
repo.emitUpdate(users)
assertEquals(users, awaitItem())
}
}
}
```
---
### Level 2 — Integration tests for the repository implementation (embedded MongoDB)
The repository implementation contains real MongoDB query logic — it must be tested
against an actual MongoDB instance. Use **Flapdoodle** for fast in-process tests (no
Docker required) or **TestContainers** for production-parity CI tests.
**`libs.versions.toml` additions:**
```toml
[versions]
flapdoodle = "4.14.0" # recheck latest before pinning
[libraries]
flapdoodle-mongo = { module = "de.flapdoodle.embed:de.flapdoodle.embed.mongo", version.ref = "flapdoodle" }
```
In the server test sourceset:
```kotlin
testImplementation(libs.flapdoodle.mongo)
```
**Repository integration test pattern:**
```kotlin
// server/user/data/UserRepositoryImplTest.kt
class UserRepositoryImplTest {
private lateinit var mongoClient: MongoClient
private lateinit var collection: MongoCollection<UserDocument>
private lateinit var repo: UserRepositoryImpl
@BeforeTest fun setUp() {
val mongod = MongodStarter.getDefaultInstance()
.prepare(MongodConfig.builder()
.version(Version.Main.PRODUCTION)
.net(Net(27017, Network.localhostIsIPv6()))
.build())
.start()
mongoClient = MongoClient.create("mongodb://localhost:27017")
collection = mongoClient
.getDatabase("test_${System.currentTimeMillis()}") // isolated DB per test class
.getCollection<UserDocument>("users")
repo = UserRepositoryImpl(collection)
}
@AfterTest fun tearDown() {
collection.drop() // clean up after each test class run
mongoClient.close()
}
@Test fun `findById returns mapped domain model`() = runTest {
val doc = UserDocument(id = "u1", name = "Alice", email = "alice@example.com")
collection.insertOne(doc)
val result = repo.findById("u1")
assertNotNull(result)
assertEquals("Alice", result.name)
assertEquals("alice@example.com", result.email)
}
@Test fun `findById returns null when document absent`() = runTest {
assertNull(repo.findById("not-there"))
}
@Test fun `save inserts then retrieves correctly`() = runTest {
val user = User(id = "u2", name = "Bob", email = "bob@example.com")
repo.save(user)
val found = repo.findById("u2")
assertEquals(user.name, found?.name)
}
@Test fun `delete removes the document`() = runTest {
collection.insertOne(UserDocument(id = "u3", name = "Carol", email = "carol@example.com"))
val deleted = repo.delete("u3")
assertTrue(deleted)
assertNull(repo.findById("u3"))
}
@Test fun `findById propagates typed error on driver exception`() = runTest {
// Close the client to force a connection error
mongoClient.close()
assertFailsWith<DatabaseError.Unknown> { repo.findById("any") }
}
}
```
---
### Testing change streams (Flow)
Change streams require a **replica set** — they do not work on a standalone Flapdoodle
instance. Options:
- Use a replica set via TestContainers (`MongoDBContainer` with replica set support)
- Or test the Flow contract via `FakeUserRepository.emitUpdate()` in unit tests (recommended
for most cases — the MongoDB change-stream plumbing is driver-owned, not yours to test)
```kotlin
// TestContainers replica set — for full change-stream integration test
@Testcontainers
class UserWatchIntegrationTest {
companion object {
@Container
val mongo = MongoDBContainer("mongo:7").apply {
withCommand("--replSet", "rs0")
}
}
@Test fun `watchUsers emits on insert`() = runTest {
val client = MongoClient.create(mongo.connectionString)
val collection = client.getDatabase("test").getCollection<UserDocument>("users")
val repo = UserRepositoryImpl(collection)
repo.watchUsers().test(timeout = 5.seconds) {
collection.insertOne(UserDocument("u1", "Alice", "alice@example.com"))
val update = awaitItem()
assertTrue(update.any { it.name == "Alice" })
}
}
}
```
---
### Testing the document mapping (pure unit test — no DB needed)
Document ↔ domain mapping is pure Kotlin — test it without any database:
```kotlin
class UserDocumentMappingTest {
@Test fun `toDomain maps all fields correctly`() {
val doc = UserDocument(id = "u1", name = "Alice", email = "alice@example.com", role = "ADMIN")
val user = doc.toDomain()
assertEquals("u1", user.id)
assertEquals("Alice", user.name)
assertEquals("alice@example.com", user.email)
assertEquals(UserRole.ADMIN, user.role)
}
@Test fun `toDocument round-trips through toDomain`() {
val user = User(id = "u1", name = "Alice", email = "alice@example.com", role = UserRole.ADMIN)
assertEquals(user, user.toDocument().toDomain())
}
}
```
---
## Common Anti-Patterns
- accessing collections directly from route handlers — always go through a repository
- leaking `BsonDocument` or `MongoCollection` types out of the data layer — map to domain types at the boundary
- using change streams for every collection by default — use them only when live updates are needed
- constructing `MongoClient` in every call instead of sharing one instance via Koin — exhausts connection pools
- catching `MongoException` globally and swallowing it — return a typed `DatabaseError` instead
- **mocking `MongoCollection` directly in tests** — mocking driver internals (`collection.findOneById(...)` returns ...) is brittle; the mock breaks on driver upgrades and doesn't test query construction; use Flapdoodle/TestContainers for the repository impl, and `FakeUserRepository` for everything above it
- **only writing service tests (Level 1) and skipping repository integration tests** — service tests with fakes verify service logic, but the repository implementation contains actual MongoDB query logic (filter construction, sort, projection); that code only executes against a real driver
- **sharing a single embedded MongoDB database across tests** — concurrent or sequential tests that share collection state interfere with each other; use a unique database name per test class (e.g. `"test_${System.currentTimeMillis()}"`) and drop it in `@AfterTest`
- **testing change streams against a standalone MongoDB** — change streams require a replica set; a standalone instance silently returns no events; either use TestContainers with `--replSet rs0` or skip the live-stream test in favour of a fake Flow in unit tests
If the connection pool is exhausted or queries are slow, check that only one `MongoClient` is registered per DI scope.
---
## Output Style
When asked about MongoDB setup or data access, respond in this order:
1. recommendation (server-side only, repository boundary, typed errors)
2. project structure (client factory, collection, repository, DI module)
3. code snippet (repository impl with one `findById` and one Flow)
4. **FakeRepository** — for service/use case unit tests
5. **Flapdoodle integration test** — one test covering the happy path and one covering not-found
6. **Document mapping test** — pure Kotlin round-trip test
7. why that approach is preferred
8. main alternative (direct collection access, other drivers)
Never ship a MongoDB repository implementation without a matching Level 1 fake and a Level 2
Flapdoodle test. Both levels cover different bugs — do not treat one as sufficient.
Keep the snippet to one repository method. Map to the user's actual collection and document names when provided.
---
## Changelog
| Date | Change |
|---|---|
| 2026-06-21 | Initial release. |More Database skills
prisma-mongodb-upgrade
prisma/skills
Decision and migration guide for Prisma ORM MongoDB projects on v6, which have no upgrade path to v7. Use when a MongoDB project asks about upgrading Prisma, when "upgrade to prisma 7" comes up in a project with provider = "mongodb", or when evaluating a move to Prisma Next. Triggers on "upgrade prisma mongodb", "prisma 7 mongodb", "mongodb prisma migration", "prisma next mongodb".
azure-upgrade
microsoft/azure-skills
Assess and upgrade Azure workloads between plans, tiers, or SKUs, or modernize Azure SDK dependencies in source code. WHEN: upgrade Consumption to Flex Consumption, upgrade Azure Functions plan, change hosting plan, function app SKU, migrate App Service to Container Apps, modernize legacy Azure Java SDKs (com.microsoft.azure to com.azure), migrate Azure Cache for Redis (ACR/ACRE) to Azure Managed Redis (AMR).
azure-cost-optimization
microsoft/azure-skills
Identify Azure cost savings from usage and spending data. USE FOR: optimize Azure costs, reduce Azure spending/expenses, analyze Azure costs, find cost savings, generate cost optimization report, identify orphaned resources to delete, rightsize VMs, reduce waste, optimize Redis costs, optimize storage costs, AKS cost analysis add-on, namespace cost, cost spike, anomaly, budget alert, AKS cost visibility. DO NOT USE FOR: deploying resources (use azure-deploy), general Azure diagnostics (use azure-diagnostics), security issues (use azure-security)

