Skip to main content

Folder Structure

Overview

Use a feature-first structure with Clean Architecture inside each feature.

This is the default recommendation for modern mobile projects because it:

  • Scales better as features and teams grow
  • Reduces cross-feature coupling
  • Keeps code ownership clear
  • Still preserves strict architectural boundaries

At the top level, organize by feature. Inside each feature, keep presentation, domain, and data. Keep cross-cutting utilities in core (or shared).

iOS (Swift / SwiftUI)

App/
├── Features/
│ ├── Auth/
│ │ ├── Presentation/
│ │ │ ├── LoginView.swift
│ │ │ └── LoginViewModel.swift
│ │ ├── Domain/
│ │ │ ├── Entities/
│ │ │ │ └── User.swift
│ │ │ ├── UseCases/
│ │ │ │ └── LoginUseCase.swift
│ │ │ └── Repositories/
│ │ │ └── AuthRepository.swift
│ │ └── Data/
│ │ ├── DTOs/
│ │ │ └── TokenDTO.swift
│ │ ├── Mappers/
│ │ │ └── UserMapper.swift
│ │ ├── Repositories/
│ │ │ └── AuthRepositoryImpl.swift
│ │ └── Sources/
│ │ ├── Remote/
│ │ │ └── AuthRemoteDataSource.swift
│ │ └── Local/
│ │ └── KeychainTokenStore.swift
│ ├── Feed/
│ │ ├── Presentation/
│ │ ├── Domain/
│ │ └── Data/
│ └── Profile/
│ ├── Presentation/
│ ├── Domain/
│ └── Data/
└── Core/
├── DI/
│ └── AppContainer.swift
├── Networking/
│ ├── HTTPClient.swift
│ └── AuthInterceptor.swift
├── DesignSystem/
│ ├── PrimaryButton.swift
│ └── AppTextField.swift
├── Logging/
│ └── Logger.swift
└── Extensions/
└── String+Validation.swift

Key Points (iOS)

  • Put feature-specific code under Features/<FeatureName>/...
  • Keep only shared, reusable infrastructure in Core
  • Avoid creating global Data or Domain folders when code belongs to a single feature

Android (Kotlin / Compose)

app/
├── features/
│ ├── auth/
│ │ ├── presentation/
│ │ │ ├── LoginScreen.kt
│ │ │ ├── LoginViewModel.kt
│ │ │ └── LoginState.kt
│ │ ├── domain/
│ │ │ ├── entity/
│ │ │ │ └── User.kt
│ │ │ ├── usecase/
│ │ │ │ └── LoginUseCase.kt
│ │ │ └── repository/
│ │ │ └── AuthRepository.kt
│ │ └── data/
│ │ ├── dto/
│ │ ├── mapper/
│ │ ├── repository/
│ │ │ └── AuthRepositoryImpl.kt
│ │ └── source/
│ │ ├── remote/
│ │ │ └── AuthApiService.kt
│ │ └── local/
│ │ └── AuthPreferences.kt
│ ├── feed/
│ │ ├── presentation/
│ │ ├── domain/
│ │ └── data/
│ └── profile/
│ ├── presentation/
│ ├── domain/
│ └── data/
└── core/
├── di/
│ ├── NetworkModule.kt
│ └── DatabaseModule.kt
├── network/
│ └── RetrofitClient.kt
├── designsystem/
│ └── PrimaryButton.kt
├── logging/
│ └── TimberLogger.kt
└── util/
└── Extensions.kt

Key Points (Android)

  • Feature packages are the primary unit of ownership
  • Keep core small and stable
  • Prefer one-way dependencies: presentation -> domain <- data

Flutter (Dart)

lib/
├── features/
│ ├── auth/
│ │ ├── presentation/
│ │ │ ├── login_page.dart
│ │ │ ├── login_bloc.dart
│ │ │ ├── login_event.dart
│ │ │ └── login_state.dart
│ │ ├── domain/
│ │ │ ├── entities/
│ │ │ │ └── user.dart
│ │ │ ├── usecases/
│ │ │ │ └── login_usecase.dart
│ │ │ └── repositories/
│ │ │ └── auth_repository.dart
│ │ └── data/
│ │ ├── models/
│ │ ├── mappers/
│ │ ├── repositories/
│ │ │ └── auth_repository_impl.dart
│ │ └── sources/
│ │ ├── remote/
│ │ │ └── auth_api.dart
│ │ └── local/
│ │ └── token_storage.dart
│ ├── feed/
│ │ ├── presentation/
│ │ ├── domain/
│ │ └── data/
│ └── profile/
│ ├── presentation/
│ ├── domain/
│ └── data/
└── core/
├── di/
│ └── injection.dart
├── network/
│ ├── dio_client.dart
│ └── auth_interceptor.dart
├── design_system/
│ └── primary_button.dart
├── storage/
│ └── storage_config.dart
└── utils/
├── constants.dart
└── extensions.dart

Key Points (Flutter)

  • Use features/<feature> as the default package boundary
  • Keep feature internals private where possible
  • Move only truly shared code into core

File Naming Conventions

iOS (Swift)

  • PascalCase for files and types: LoginViewModel.swift, AuthRepository.swift
  • Common suffixes: View, ViewModel, UseCase, Repository, DTO

Android (Kotlin)

  • PascalCase for files and types: LoginScreen.kt, LoginUseCase.kt
  • Common suffixes: Screen, ViewModel, UseCase, Repository, Dto

Flutter (Dart)

  • snake_case for files: login_page.dart, auth_repository.dart
  • PascalCase for types: LoginPage, AuthRepository
  • Common suffixes: Page, Bloc, Event, State, UseCase, Repository, Dto

Migration Path (Layer-First to Feature-First)

If your project currently uses top-level presentation/domain/data folders, migrate incrementally:

  1. Create features/<feature> for one vertical slice (e.g., auth)
  2. Move its presentation/domain/data files together
  3. Keep existing global layers for untouched features
  4. Repeat feature by feature
  5. Remove global layer folders once migration completes

This keeps risk low and avoids large refactors in one PR.

Testing Structure

Mirror the source tree by feature:

iOS

AppTests/
├── Features/
│ └── Auth/
│ ├── Presentation/
│ │ └── LoginViewModelTests.swift
│ ├── Domain/
│ │ └── LoginUseCaseTests.swift
│ └── Data/
│ └── AuthRepositoryImplTests.swift
└── Core/
└── Networking/
└── HTTPClientTests.swift

Android

test/
├── features/
│ └── auth/
│ ├── presentation/
│ │ └── LoginViewModelTest.kt
│ ├── domain/
│ │ └── LoginUseCaseTest.kt
│ └── data/
│ └── AuthRepositoryImplTest.kt
└── core/
└── network/
└── RetrofitClientTest.kt

Flutter

test/
├── features/
│ └── auth/
│ ├── presentation/
│ │ └── login_bloc_test.dart
│ ├── domain/
│ │ └── login_usecase_test.dart
│ └── data/
│ └── auth_repository_impl_test.dart
└── core/
└── network/
└── dio_client_test.dart

Summary

For modern mobile apps, prefer:

  • Feature-first organization for product code
  • Clean Architecture boundaries inside each feature
  • A small core/shared module for reusable infrastructure

This structure is easier to scale, easier to assign ownership, and easier to evolve over time.