Skip to content

Modularization with Swift Package Manager

A module is a compiler-enforced dependency boundary. It can protect core behavior from UI and storage frameworks, but every module adds manifests, public API decisions, build graph edges, and coordination cost.

Start with an inward graph

FieldNotesSwiftUI ----+
|
FieldNotesUIKit ------+--> FieldNotesApplication --> FieldNotesDomain
|
SwiftDataAdapter -----+

The domain target imports no Apple UI, database, or network framework. The application target owns use cases and ports. App targets compose selected adapters.

// Package.swift excerpt
.target(name: "FieldNotesDomain"),
.target(
name: "FieldNotesApplication",
dependencies: ["FieldNotesDomain"]
),
.target(
name: "FieldNotesPersistence",
dependencies: ["FieldNotesApplication", "FieldNotesDomain"]
)

Extract along stable seams

Useful signals include reuse by two app targets, independent ownership, slow incremental builds, a dangerous dependency that should not spread, or a core that needs command-line tests. A folder can express organization before a package is justified.

Design the public surface

Moving code into a package forces access-control choices. Export use cases and domain values that clients need, not internal helpers or database shapes. Avoid a catch-all shared module that becomes a new global namespace.

Do not start with dozens of feature and utility packages. Measure build time and coordination, then split the dependency graph where a concrete outcome improves.

Series navigation

References