name: layered-module-patterns description: 'Reference matrix for how domain and data modules are wired into feature, common, and task modules. Use when choosing the right repository/use-case/source shape or reviewing docs against real code.'
Skill: Layered Module Patterns
Overview
AniTrend does not have a single universal module recipe. The stable rules are the layer boundaries and contract shapes; the concrete wiring varies by whether the module is query-only, mutation-only, or hybrid.
Use this skill to choose the closest existing pattern before adding or documenting a new module.
For the concrete review spine and Android helper anchors, use the
layer example matrix.
The Rules section below is authoritative. If a rule conflicts with the pattern matrix or the notes, the Rules section wins.
Pattern matrix
| Pattern | Reference module | When to copy it | Domain shape | Data shape | Entry-point shape |
|---|---|---|---|---|---|
| Simple query-only | domain/tag + data/tag |
Small non-paged read-only collections or singleton/detail lookups | Single repository contract + abstract use case | Single repository + non-paged source + TagUseCaseImpl |
Feature/common code reads through interactor |
| Read-heavy multi-contract | domain/media + data/media |
Detail, paged, and network variants in one domain package | Nested repository contracts such as Detail, Paged, Network |
Types.kt aliases + MediaRepository.* + MediaInteractor.* |
Feature ViewModels consume read interactors |
| Hybrid query + mutation | domain/medialist + data/medialist + task/medialist |
Fetch plus save/delete/sync for the same concept | Nested contracts per operation | Separate source/repository/interactor classes per operation | Feature/common code reads directly, mutations usually enqueue task workers |
| Hybrid fetch + action | domain/review + data/review + task/review |
Paged/detail reads plus vote/save/delete writes | Nested contracts per read/write action | Separate read and mutation sources, repositories, and interactors | Review UI fetches via feature ViewModel; voting/deleting routes through task workers |
| Mutation-only | domain/favourite + data/favourite + task/favourite |
A focused toggle/save/delete operation with no local read screen | Small sealed param + single repository contract | Lean Types.kt, single repository, single interactor bridge |
UI/common code creates task params; worker runs the mutation interactor |
| Android/platform support | android/core + android/navigation + android/deeplink |
Shared platform helpers, shell navigation, deep-link entry, or internal Android API reuse | N/A | Koin-backed helpers, providers, controllers, and shell content | app/core, app shell, and entry layers consume the platform API |
Key files to read
domain/src/main/kotlin/co/anitrend/domain/media/data/src/main/kotlin/co/anitrend/data/media/domain/src/main/kotlin/co/anitrend/domain/medialist/data/src/main/kotlin/co/anitrend/data/medialist/task/medialist/src/main/kotlin/co/anitrend/task/medialist/domain/src/main/kotlin/co/anitrend/domain/review/data/src/main/kotlin/co/anitrend/data/review/task/review/src/main/kotlin/co/anitrend/task/review/domain/src/main/kotlin/co/anitrend/domain/favourite/data/src/main/kotlin/co/anitrend/data/favourite/task/favourite/src/main/kotlin/co/anitrend/task/favourite/
Standard wiring
feature ViewModel / common presenter / task Worker
-> data.*Interactor alias
-> domain use case
-> domain repository contract
-> data repository
-> data source
-> GraphQL controller / Room / cache
The public dependency surface that feature and task modules usually see is the interactor alias
exported from the data module Types.kt. That alias resolves to a domain use case specialized
with DataState<T>.
If the entry-layer change depends on theme/configuration, deep links, drawer shell behavior, or other Android helper APIs, stop here and pair this matrix with android-platform-patterns.
Rules
Layer boundary rules
- Keep params, repository contracts, and abstract use cases in
:domain. - Keep concrete repositories, sources, controllers, converters, mappers, and Koin bindings in
:data. - Keep
Types.ktlimited to controller aliases, repository aliases, and interactor aliases only. - Use
tagas the smallest baseline only. Do not treat it as the canonical pattern for media, review, or other mutation-heavy modules.
Source wiring rules
- For offline-first non-paged read flows, wire the repository as
source create source(param)over a source that extendsAbstractCoreDataSourceand returns a local observable flow. - For offline-first paged read flows, wire the repository as
source create source(param)over a source that reads from Room viaobservable(): Flow<PagedList<T>>. The source may coordinate refresh timing and paging triggers, but it should not convert remote payloads into domain models inline or keep a separate network-only paging path when a local source-of-truth exists. - Mappers own persistence. If the local table shape depends on request context such as a parent id or page number, pass that context into the mapper before invoking the controller.
- Converters own projection from local entity or view types into domain models consumed by the
FlowPagedListBuilderpipeline.
Import boundary rules
- Imports like
co.anitrend.data.review.GetReviewPagedInteractorinfeatureortaskmodules are acceptable because they alias domain use cases. Imports ofReviewRepository,ReviewSourceImpl,ReviewMapper, or remote models are not. - Do not recreate reusable Android-side helpers in
:feature:*,:common:*, or:task:*when:android:*or:app:corealready owns the platform abstraction.
Design heuristics
- Split repository contracts by operation when the module mixes detail/paged/sync/save/delete/rate behavior. Do not collapse unrelated flows into one broad repository API.
- A module is hybrid when it performs both read operations and write operations within the same feature boundary.
- Prefer task-backed mutation flows when the existing user flow already uses WorkManager or when
the write must survive process death.
medialist,review, andfavouriteare the current references.
Offline-first read notes
Non-paged offline-first reads
- Use
TagSource,GenreSource,EdgeConfigSource,MediaSource.Detail,ReviewSource.Entry, andUserSource.Identifier/Viewer/Profile/Statisticas the baseline references. - Sources should return
Flow<Model>orFlow<List<Model>>, notPagedList. - The source
invoke(...)operator should store query context, callcachePolicy(...), and then return the localobservable()flow. observable()should read Room-backed local state and project it with converters, typically across IO and computation dispatcher boundaries.- Multi-context entity families should model distinct source variants as inner classes instead of collapsing unrelated query contexts into one broad source contract. A family is multi-context when the same entity type is fetched through at least two structurally different query shapes, such as a detail lookup by id and a paged list filtered by criteria.
- Mutation-only variants such as review save/rate/delete or user follow/update are separate source shapes and should not be used as the baseline for non-paged read contracts.
Paged offline-first reads
- Use
MediaSource.Paged,AiringScheduleSource.Paged, andUserSource.Searchas the baseline for paged offline-first reads. - Sources should coordinate refreshes and paging triggers, not convert remote payloads into domain models inline.
- Mappers own persistence. If the local table shape depends on request context such as a parent id or page number, pass that context into the mapper before invoking the controller.
- Converters own projection from local entity or view types into domain models consumed by the
FlowPagedListBuilderpipeline. - Relationship collections such as media-to-character or media-to-staff should use dedicated connection entities instead of trying to reuse the remote edge model as the local cache shape.
Choosing the nearest reference
- Copy
tagwhen the module is a single non-paged read path with no task or mutation plumbing. - Copy
mediawhen you need multiple read contracts against the same entity package. - Copy
medialistwhen reads and writes share one domain area and writes are background-task driven. - Copy
reviewwhen the module fetches entities but actions such as vote/save/delete are separate mutation paths. - Copy
favouritewhen the job is only a single toggle-style mutation. - Use
android-platform-patternswhen the work is really about reusing or extending Android shell, configuration, theme, notification, or deep-link infrastructure.