Skip to content
sysout.dev

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 (imports location.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.

core.designsystem

Purpose: Reusable UI components and theme.

Contents: * component/ - Composables used across features (cards, buttons, dialogs) * theme/ - Material3 color schemes, typography, shapes

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.

2. Register Entity in AppDatabase

Update core.database.AppDatabase.kt entities list and provide DAO accessor.

3. Add DI Provider

Update app.di.DataModule.kt to provide DAO and Repository.

4. Wire Navigation

Add route to app.navigation.NavGraph.kt.

5. Grow Structure as Needed

Add domain/, data/, ui/ subfolders only when file count exceeds 10.

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