Architecture With Nico

Practical software architecture and design for iOS engineers.

Feature Modules in iOS: Vertical Slices vs Layers

Folder structure is not architecture, but it can reveal architecture.

It can also hide it.

An iOS app can be organized by technical layers, by features, by packages, by screens, by domains, or by some mixture of all of them. None of these shapes is automatically correct. The right organization is the one that makes change easier for the team working in that codebase.

Still, there is one question worth asking often:

When a feature changes, how much of the project do you have to touch?

#Layer-Based Organization

A classic structure groups code by technical role:

Sources/
  Networking/
  Persistence/
  Views/
  ViewModels/
  Repositories/
  UseCases/
  Models/

This can be easy to understand at first because every file has an obvious category. If you create a ViewModel, it goes in ViewModels. If you create a repository, it goes in Repositories.

The problem appears when product work is feature-shaped.

Adding favorites might require touching:

Views/FavoritesView.swift
ViewModels/FavoritesViewModel.swift
UseCases/LoadFavoritesUseCase.swift
Repositories/FavoritesRepository.swift
Models/Favorite.swift
Networking/FavoritesAPI.swift

The feature is spread across the app. The folder structure tells you what kind of object each file is, but not what product capability those files belong to.

#Feature-Based Organization

A feature-based structure groups code by product capability:

Sources/
  Features/
    Products/
      ProductsView.swift
      ProductsViewModel.swift
      LoadProductsUseCase.swift
      ProductsRepository.swift
    Favorites/
      FavoritesView.swift
      FavoritesViewModel.swift
      LoadFavoritesUseCase.swift
      FavoritesRepository.swift
  Shared/
    Networking/
    Persistence/
    DesignSystem/

Now a feature's UI, state, use cases, and local abstractions live close together. When favorites changes, you mostly work in Features/Favorites.

This is a vertical slice. It organizes around user-facing capability instead of technical category.

#Vertical Does Not Mean Everything Is Duplicated

A bad version of feature slicing duplicates shared infrastructure in every feature.

That is not the goal.

Networking clients, design system components, persistence primitives, analytics clients, and shared domain concepts can still live outside features.

The distinction is ownership.

If a type belongs to one feature's behavior, keep it in the feature. If it is a stable cross-feature tool, move it to shared infrastructure.

Features/
  Checkout/
    PlaceOrderUseCase.swift
    CheckoutViewModel.swift
    CheckoutView.swift
Shared/
  Networking/
    HTTPClient.swift
  Analytics/
    AnalyticsTracking.swift
  DesignSystem/
    PrimaryButton.swift

The checkout feature can depend on shared tools without losing its own boundary.

#Layers Inside a Feature

Feature-based organization does not mean layers disappear. It means layers become local.

Features/
  Products/
    UI/
    Domain/
    Data/
    Composition/

This can be useful for larger features. A small feature may not need subfolders at all. A larger feature may benefit from local structure.

The key is that the feature remains the top-level unit of change.

#When Packages Help

Swift packages can enforce boundaries more strongly than folders.

A folder says, "Please do not import this from there." A package can make that impossible or at least explicit.

Modularizing into packages can help when:

  • multiple teams own different features
  • build times are painful
  • boundaries are frequently violated
  • features need independent previews or test targets
  • shared code has become too easy to import casually

But packages add cost. They introduce manifests, targets, dependency rules, and sometimes resource management complexity. Do not split into packages just to make the project look modular.

Start with folders. Move to packages when enforcement is worth the overhead.

#Dependency Direction

Feature modules should depend inward or sideways through stable shared abstractions, not randomly across product features.

This is suspicious:

Checkout imports Products
Products imports Favorites
Favorites imports Checkout

Those dependencies make features harder to change independently.

Instead, shared concepts can move to a common module:

Checkout imports CatalogDomain
Products imports CatalogDomain
Favorites imports CatalogDomain

The goal is not zero dependencies. The goal is understandable dependencies.

#Composition Across Features

If features are isolated, where do they connect?

At composition boundaries.

The app composition root can assemble shared infrastructure and pass dependencies into feature factories:

struct AppCompositionRoot {
    let httpClient: HTTPClient
    let analytics: AnalyticsTracking

    func makeProductsFeature() -> ProductsFeatureFactory {
        ProductsFeatureFactory(
            repository: RemoteProductsRepository(httpClient: httpClient),
            analytics: analytics
        )
    }
}

This keeps feature modules from creating concrete infrastructure on their own. It also keeps the app-level dependency graph visible.

See The Composition Root in SwiftUI Apps for a concrete version of this idea.

#Choosing a Structure

I would not use the same structure for every app.

For a small app:

Features/
Shared/

For a medium app:

Features/
  Products/
    UI/
    Domain/
    Data/
  Checkout/
Shared/
  Networking/
  Persistence/
  DesignSystem/

For a large app:

Packages/
  ProductsFeature/
  CheckoutFeature/
  SharedNetworking/
  DesignSystem/
App/
  CompositionRoot.swift

The structure should grow with the reasons for it.

#What You Should Remember

Layer-based organization groups files by what they are. Feature-based organization groups files by what they change with.

Most product work is feature-shaped, so vertical slices often make iOS apps easier to navigate and change. Keep shared infrastructure shared, keep feature behavior close to the feature, and use packages only when stronger boundaries are worth the cost.

Architecture is not the folder tree, but the folder tree should make the architecture easier to see.