androidengineers.Book a session

Code Organization

Package by Feature vs Layer

article20 minMedium

How you organize code inside a module matters as much as the module structure itself. The two dominant approaches — package by layer and package by feature — have very different implications for how a codebase scales.

Package by Layer

Groups all classes of the same type together, regardless of the feature they belong to:

com.example.myapp/
├── data/
│   ├── ArticleRepository.kt
│   ├── UserRepository.kt
│   └── SettingsRepository.kt
├── domain/
│   ├── GetArticlesUseCase.kt
│   ├── GetUserUseCase.kt
│   └── UpdateSettingsUseCase.kt
├── ui/
│   ├── ArticleListFragment.kt
│   ├── UserProfileFragment.kt
│   └── SettingsFragment.kt
└── network/
    ├── ArticleApi.kt
    └── UserApi.kt

Pros: Easy to find all classes of a given type (all DAOs in one place). Good for small teams and small codebases.

Cons: Adding a new feature requires touching every layer folder. Related code is scattered. Hard to extract a feature into its own module later.

Package by Feature

Groups all code for a given feature together, regardless of what layer it's in:

com.example.myapp/
├── feed/
│   ├── FeedFragment.kt
│   ├── FeedViewModel.kt
│   ├── FeedRepository.kt
│   ├── FeedApi.kt
│   └── FeedAdapter.kt
├── profile/
│   ├── ProfileFragment.kt
│   ├── ProfileViewModel.kt
│   ├── ProfileRepository.kt
│   └── ProfileApi.kt
└── settings/
    ├── SettingsFragment.kt
    ├── SettingsViewModel.kt
    └── SettingsRepository.kt

Pros: All code for a feature is co-located — easy to understand and modify. Natural boundary for ownership. Easy to extract into a module (just move the package).

Cons: Harder to see cross-cutting patterns (all DAOs are spread across packages). Can encourage coupling between features if not careful.

Recommendation: Feature-First, Layer-Second

The best approach combines both: packages organized by feature at the top level, with sub-packages by layer within each feature:

com.example.myapp/
├── feed/
│   ├── data/
│   │   ├── FeedRepository.kt
│   │   └── FeedApi.kt
│   ├── domain/
│   │   └── GetFeedUseCase.kt
│   └── ui/
│       ├── FeedFragment.kt
│       └── FeedViewModel.kt
├── profile/
│   ├── data/ ...
│   ├── domain/ ...
│   └── ui/ ...
└── core/
    ├── network/       ← shared infrastructure
    ├── database/
    └── ui/            ← shared UI components

Kotlin Visibility and Package Structure

Use internal visibility to enforce feature boundaries:

// feed/data/FeedRepository.kt
internal class FeedRepository(...)  // ← only feed package can use this

// feed/domain/GetFeedUseCase.kt
class GetFeedUseCase(private val repo: FeedRepository) {  // ← public: other packages can use the use case
    operator fun invoke() = repo.getArticles()
}

External packages use GetFeedUseCase but never access FeedRepository directly.

Transitioning from Layer to Feature

Practical migration path:

  1. Pick the least-coupled feature first
  2. Create a feature sub-directory
  3. Move all related files there
  4. Adjust internal visibilities
  5. Repeat for each feature

At the end, what remains in the old layer folders becomes your core shared infrastructure.

Key Takeaways

ApproachBest for
By layerSmall apps (< 5 features), single developer
By featureMedium to large apps; teams with feature ownership
Feature-first + layer-secondBest of both: co-location + architectural clarity
internal visibilityNatural enforcement of feature boundaries

YOUR LEARNING JOURNEY

0 of 177 available lessons completed

Progress saved in this browser. No account needed.
Package by Feature vs Layer | Android System Design | Android Engineers