Data Module Creation Skill
This skill creates a new implementation module in the data/ package.
Prerequisites & Pre-steps
Before generating the module, confirm:
- Module Naming:
{domain}: Must be snake_case (e.g.,photo_tag).{Domain}: Must be PascalCase (e.g.,PhotoTag).
- Corresponding domain module (
domain.{domain}.api) must already exist. - Structure type (see below — Simple or Full?)
- Data sources required (Remote API, Room DB, DataStore, local file system)
Structure Type
Simple — di/ + repository/ only
Use when the module only wraps a local data source (Room DAO, DataStore, ContentProvider, system API). No remote response mapping, no business logic beyond simple delegation.
Real examples: data:device, data:gallery, data:file
data/{domain}/impl/src/main/java/com/metasearch/android/data/{domain}/impl/
├── repository/
│ └── {Domain}RepositoryImpl.kt
└── di/
└── {Domain}DataGraph.kt
Full — di/ + mapper/ + repository/ + usecase/
Use when the module involves remote API calls (needs response-to-model mapping) or contains UseCase implementations with business logic.
Real examples: data:analysis, data:person, data:search
data/{domain}/impl/src/main/java/com/metasearch/android/data/{domain}/impl/
├── repository/
│ └── {Domain}RepositoryImpl.kt
├── usecase/
│ └── {Action}{Domain}UseCaseImpl.kt
├── mapper/
│ └── ResponseToModel.kt
└── di/
└── {Domain}DataGraph.kt
Implementation Steps
1. Gradle Configuration (data/{domain}/impl/build.gradle.kts)
Simple:
plugins {
alias(libs.plugins.metasearch.android.library)
alias(libs.plugins.metro)
alias(libs.plugins.metasearch.test)
}
android {
namespace = "com.metasearch.android.data.{domain}.impl"
}
dependencies {
implementation(projects.core.di)
implementation(projects.domain.{domain}.api)
implementation(projects.core.room.api) // if Room DB is needed
implementation(projects.core.datastore.api) // if DataStore is needed
implementation(libs.androidx.core.ktx)
}
Full:
plugins {
alias(libs.plugins.metasearch.android.library)
alias(libs.plugins.metasearch.kotlin.library.serialization) // if serialization is needed
alias(libs.plugins.metro)
alias(libs.plugins.metasearch.test)
}
android {
namespace = "com.metasearch.android.data.{domain}.impl"
}
dependencies {
implementation(projects.core.common)
implementation(projects.core.di)
implementation(projects.data.remote)
implementation(projects.domain.{domain}.api)
implementation(projects.core.room.api) // if Room DB is needed
implementation(projects.core.datastore.api) // if DataStore is needed
implementation(libs.androidx.core.ktx)
}
2. Key Files
Repository Implementation (repository/{Domain}RepositoryImpl.kt)
package com.metasearch.android.data.{domain}.impl.repository
import com.metasearch.android.core.common.utils.runSuspendCatching
import com.metasearch.android.core.di.scope.DataScope
import com.metasearch.android.domain.{domain}.api.repository.{Domain}Repository
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.SingleIn
@SingleIn(DataScope::class)
@Inject
class {Domain}RepositoryImpl(
// TODO: Inject data sources (Client, DAO, DataSource, etc.)
) : {Domain}Repository {
// TODO: Implement repository methods
}
UseCase Implementation — Full only (usecase/{Action}{Domain}UseCaseImpl.kt)
package com.metasearch.android.data.{domain}.impl.usecase
import com.metasearch.android.core.common.utils.runSuspendCatching
import com.metasearch.android.core.di.scope.DataScope
import com.metasearch.android.domain.{domain}.api.usecase.{Action}{Domain}UseCase
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.SingleIn
@SingleIn(DataScope::class)
@Inject
class {Action}{Domain}UseCaseImpl(
// TODO: Inject required repositories
) : {Action}{Domain}UseCase {
override suspend fun invoke(/* params */): Result<{Model}> = runSuspendCatching {
// TODO: Implement
}
}
Mapper — Full only (mapper/ResponseToModel.kt)
package com.metasearch.android.data.{domain}.impl.mapper
import com.metasearch.android.data.domain.{DomainModel} // e.g., import com.metasearch.android.data.domain.PhotoTag
import com.metasearch.android.data.remote.{domain}.response.{Domain}Response
internal fun {Domain}Response.toModel(): {DomainModel} = {DomainModel}(
// TODO: Map response fields to domain model fields
)
DI Graph (di/{Domain}DataGraph.kt)
Simple:
package com.metasearch.android.data.{domain}.impl.di
import com.metasearch.android.core.di.scope.DataScope
import com.metasearch.android.data.{domain}.impl.repository.{Domain}RepositoryImpl
import com.metasearch.android.domain.{domain}.api.repository.{Domain}Repository
import dev.zacsweers.metro.Binds
import dev.zacsweers.metro.ContributesTo
@ContributesTo(DataScope::class)
interface {Domain}DataGraph {
@Binds
val {Domain}RepositoryImpl.bind: {Domain}Repository
}
Full:
package com.metasearch.android.data.{domain}.impl.di
import com.metasearch.android.core.di.scope.DataScope
import com.metasearch.android.data.{domain}.impl.repository.{Domain}RepositoryImpl
import com.metasearch.android.data.{domain}.impl.usecase.{Action}{Domain}UseCaseImpl
import com.metasearch.android.domain.{domain}.api.repository.{Domain}Repository
import com.metasearch.android.domain.{domain}.api.usecase.{Action}{Domain}UseCase
import dev.zacsweers.metro.Binds
import dev.zacsweers.metro.ContributesTo
@ContributesTo(DataScope::class)
interface {Domain}DataGraph {
@Binds
val {Domain}RepositoryImpl.bind: {Domain}Repository
@Binds
val {Action}{Domain}UseCaseImpl.bind: {Action}{Domain}UseCase
}
3. Registration
Add to settings.gradle.kts in alphabetical order:
include(":data:{domain}:impl")
4. Sync & Verify
./gradlew :data:{domain}:impl:compileDebugKotlin
Naming & Architecture Rules
| Item | Convention | Example (domain=photo_tag) |
|---|---|---|
| Module Directory | snake_case | photo_tag |
| Class Prefix | PascalCase | PhotoTag |
| Namespace | com.metasearch.android.data.{domain}.impl |
com.metasearch.android.data.photo_tag.impl |
| Repository Impl | {Domain}RepositoryImpl |
PhotoTagRepositoryImpl |
| UseCase Impl | {Verb}{Domain}UseCaseImpl |
GetPhotoTagUseCaseImpl |
| DI Graph | {Domain}DataGraph |
PhotoTagDataGraph |
| Mapper | internal fun {Response}.toModel() |
fun PhotoTagResponse.toModel() |
| Settings (Gradle) | :data:{domain}:impl |
:data:photo_tag:impl |
Best Practices
- Domain contract first: The corresponding
domain.{domain}.apimodule must exist before creating a data module. @SingleIn(DataScope::class): All implementations must be scoped toDataScopeto avoid redundant instances.@Bindsproperty syntax: Useval {Impl}.bind: {Interface}— not function syntax — as per the project convention.runSuspendCatching: Wrap all suspend calls inrunSuspendCatchingfromcore:commonfor consistentResultwrapping.- Mapper visibility: Mapper extension functions must be
internal— they are an implementation detail of the data module. - No domain logic: Data modules only translate and deliver data. Business logic belongs in the UseCase implementations, not in repositories.
- Remote is shared: Never create a new
data:remotesub-module. Add new Retrofit services directly to the existingdata:remotemodule. - Domain models only: Use models from
data:domain. Do not define new data classes in impl modules.