Package Structure
Homey's domain-driven feature package structure and dependency boundaries.
Overview
Homey uses a domain-driven, feature-based architecture. The structure separates shared domain models and infrastructure (core) from feature-specific UI workflows (app.features).
Structure
dev.sysout.homey/
├── app/ # Application layer
│ ├── HomeyApplication.kt # Android Application class
│ ├── MainActivity.kt # Single-activity architecture
│ ├── di/ # Global dependency injection
│ ├── navigation/ # App-level navigation graph
│ └── features/ # Feature packages
│ ├── digitaltwin/ # Zone/Unit/Cell browser
│ ├── smartscan/ # OCR/PDF ingestion
│ ├── mealplanner/ # Menu architect (owner view)
│ ├── chefview/ # Daily execution view
│ └── profile/ # User settings
│
└── core/ # Foundation layer
├── domain/ # Business models & contracts
│ ├── model/ # Item, Location, etc.
│ └── repository/ # Repository interfaces
├── data/ # Persistence & sync
│ ├── database/ # Room database & DAOs
│ ├── repository/ # Repository implementations
│ └── sync/ # Cloud sync logic (future)
└── designsystem/ # UI foundation
├── component/ # Reusable composables
└── theme/ # Colors, typography, shapes
Guiding Principles
1. Package-by-Feature (Vertical Slices)
Each feature owns its complete vertical slice: domain models, data access, and UI. This ensures:
-
High cohesion - everything related to a feature is together
-
Easy to understand - one package tells the whole story
-
Simple to extract or delete - remove one folder, remove entire feature
-
Clear ownership - each feature has defined boundaries
Example: app.features.location contains Location domain model, LocationEntity, LocationDao, LocationRepository, and LocationScreen.
2. Feature Dependencies
Features may import from other features when necessary. This creates a dependency graph:
-
location- Browse and manage physical locations (no dependencies) -
item- Track items in locations (importslocation.Location)
Rule: Extract to core only when 3+ features need the same model.
3. Progressive Structure
Start flat, add folders when complexity demands:
-
≤ 10 files: Keep all files at feature root (current: location has 7, item has 10)
-
11-20 files: Add
ui/for screens, keep data/domain at root -
21+ files: Use full
ui/,data/,domain/structure
All features in the codebase use flat structure (≤10 files each).
Naming Conventions
Package Names
Use singular, lowercase, no separators:
-
✅
location,item,mealplan(singular - the feature is about "location management", not "locations") -
❌
locations,items,meal_planner
Rationale: Package names represent the concept/domain, not a collection of instances.
File Names
Use PascalCase with type suffix:
-
Domain:
Location.kt,Item.kt,GroceryItem.kt -
Screens:
LocationScreen.kt,ItemScreen.kt -
ViewModels:
LocationViewModel.kt,ItemViewModel.kt -
Repositories:
LocationRepository.kt(interface),LocalLocationRepository.kt(implementation) -
Entities:
LocationEntity.kt,ItemEntity.kt -
DAOs:
LocationDao.kt,ItemDao.kt
The type suffix makes files self-documenting - no need for technical layer folders in small features.
Core Layer Details
core.database
Purpose: Shared database infrastructure only (no domain models or DAOs).
Contents:
* AppDatabase.kt - Room database instance and configuration
* converter/ - Type converters (Date, UUID, etc.)
Scope: Database configuration only. Features own their domain models, entities, DAOs, and repositories.
Repository Pattern
Each feature uses the Repository pattern with two implementations:
// Repository interface (contract)
interface LocationRepository {
fun getRootLocations(): Flow<List<Location>>
suspend fun addLocation(name: String, parentId: String?)
}
// Local implementation (Room-based)
class LocalLocationRepository(
private val dao: LocationDao
) : LocationRepository {
// Maps between LocationEntity (database) and Location (domain)
}
// Future: Cloud sync implementation
class SyncLocationRepository(
private val dao: LocationDao,
private val api: LocationApi,
private val syncManager: SyncManager
) : LocationRepository {
// Handles local + remote sync, conflict resolution
}
Why Repository + DAO? * Repository = domain-level abstraction (works with Location domain model) * DAO = database-level access (works with LocationEntity) * Allows swapping local-only → cloud-sync implementations without changing UI code
Feature Layer Examples
Flat Structure (≤10 files)
location feature (7 files):
app.features.location/
├── Location.kt # Domain model
├── LocationRepository.kt # Repository interface
├── LocalLocationRepository.kt # Implementation (Room-based)
├── LocationEntity.kt # Database entity
├── LocationDao.kt # Room DAO
├── LocationScreen.kt # Composable UI
└── LocationViewModel.kt # State management
item feature (10 files):
app.features.item/
├── Item.kt # Domain model (sealed class)
├── GroceryItem.kt # Domain subtype
├── ItemRepository.kt # Repository interface
├── LocalItemRepository.kt # Implementation
├── ItemEntity.kt # Base database entity
├── GroceryItemEntity.kt # Type-specific entity
├── ItemDao.kt # Room DAO
├── GroceryItemWithDetails.kt # Room relation
├── ItemScreen.kt # Composable UI
└── ItemViewModel.kt # State management
No ui/, data/, or domain/ subfolders when file count is ≤10. Everything lives at feature root.
Structured Feature (11+ files)
When a feature grows beyond 10 files, add structure:
app.features.mealplan/
├── domain/ # Domain models
│ ├── MealPlan.kt
│ ├── Recipe.kt
│ └── ShoppingList.kt
├── data/ # Data access
│ ├── MealPlanRepository.kt
│ ├── LocalMealPlanRepository.kt
│ ├── MealPlanEntity.kt
│ └── MealPlanDao.kt
└── ui/ # User interface
├── MealPlanScreen.kt
├── RecipeDetailScreen.kt
├── MealPlanViewModel.kt
└── components/
└── RecipeCard.kt
Adding a New Feature
1. Create Feature Package
app.features.yourfeature/
├── YourDomainModel.kt # Domain model (data class)
├── YourRepository.kt # Repository interface
├── LocalYourRepository.kt # Repository implementation
├── YourEntity.kt # Database entity
├── YourDao.kt # Room DAO
├── YourScreen.kt # Composable UI
└── YourViewModel.kt # State management
Start flat - create all files at feature root.
Multi-Module Strategy (Future)
Current architecture is single-module (:app). Multi-module split is deferred until:
-
3+ developers working simultaneously
-
50k+ lines of code
-
Build time exceeds 2 minutes
Future modules:
:app # Main application module
:core:domain # Pure Kotlin domain models
:core:data # Database & repositories
:core:designsystem # Compose UI components
:feature:digitaltwin # Digital twin feature
:feature:mealplanner # Meal planner feature
Benefits: Parallel compilation, enforced boundaries, faster incremental builds.
Exploration: Alternative Architectures Considered
Alternative 1: Package-by-Layer (Rejected)
Traditional Android structure with shared domain in core:
dev.sysout.homey/
├── core/
│ ├── domain/model/ # All domain models
│ ├── domain/repository/ # All repository interfaces
│ └── data/repository/ # All implementations
└── app/features/
├── location/ui/ # UI only
└── item/ui/ # UI only
Rejected because:
-
Low cohesion - Location domain model is in core, but only location feature uses it
-
Changing a feature requires editing files in core + feature
-
Hard to understand ownership ("who owns Location.kt?")
-
Core becomes a dumping ground for anything "shared"
Alternative 2: Package-by-Layer (No Core Domain)
dev.sysout.homey/
├── ui/location/
├── ui/item/
├── data/repository/
├── data/dao/
└── data/entity/
Rejected because:
-
Very low cohesion - 5 packages to understand one feature
-
No clear boundaries between features
-
Doesn’t scale as feature count grows
Alternative 3: Pure Package-by-Feature (No Sharing)
Each feature duplicates its dependencies:
features/
├── location/
│ ├── Location.kt # Duplicated
│ └── LocationScreen.kt
└── item/
├── Location.kt # Duplicated!
└── ItemScreen.kt
Rejected because:
-
Duplicated models across features
-
No shared source of truth
-
Database schema conflicts
Why Current Architecture Wins
Chosen approach: Package-by-Feature at All Levels
-
✅ High cohesion - everything about location is in
app.features.location -
✅ Easy to understand - one folder tells complete story
-
✅ Simple to extract - remove folder, remove entire feature
-
✅ Features import from other features when needed (item → location)
-
✅ Core contains only true infrastructure (database config, design system)
-
✅ Flat structure keeps small features simple
When to share? Extract to core only when 3+ features need the same model. Currently:
* Location lives in location feature (only item imports it)
* If a third feature needs Location, consider moving to core.location