← All topics/Architecture & design patterns

Practical interview questions

Scenario-style prompts with sample answer outlines. Focus is on how you would design and reason in real codebases.

Question 5

Feature modules, SPM packages, and build graphs

Build times are growing and teams step on each other in one Xcode target. What modularization strategy would you propose, and what mistakes make modules worse?

Follow-ups

  • How do you prevent circular dependencies?

Answer outline

Split by feature verticals (SearchFeature, CheckoutFeature) plus shared kits (DesignSystem, Networking, Analytics). Each SPM target exposes only public types at its boundary — implementation details stay internal.

  1. 1.Merge conflicts in the shared .pbxproj shrink significantly — feature teams rarely touch the same targets.
  2. 2.Incremental build times drop — Xcode only recompiles targets downstream of your change.
  3. 3.Accidental cross-feature coupling is caught at compile time rather than in code review.

Common mistakes: cyclic imports between feature modules, a shared Utils target that becomes a dumping ground, and modularising too early before the team pain justifies the overhead.

Principles

  • Enforce a one-way dependency direction: Features → Domain/Core → Infrastructure — nothing flows back up.
  • Only public types cross module boundaries; keep implementation details internal to preserve the seam.
  • Avoid a catch-all Utils module — split by concern (DesignSystem, Logging) so each target has a clear purpose.
  • Prevent circular dependencies by placing shared contracts in a low-level Domain or CoreTypes target both sides can import.

Features depend on Domain and shared kits; nothing in Domain or kits depends on a feature:

Package.swift — module dependency graph
let package = Package(
    name: "AppModules",
    targets: [
        .target(name: "Domain"),           // no app dependencies
        .target(name: "Networking",
                dependencies: ["Domain"]),
        .target(name: "DesignSystem"),
        .target(name: "ProfileFeature",
                dependencies: ["Domain", "Networking", "DesignSystem"]),
        .target(name: "SearchFeature",
                dependencies: ["Domain", "Networking", "DesignSystem"]),
    ]
)

Follow-up angles

  • Circular dependencies: introduce a CoreTypes or SharedInterfaces target that both sides import — neither depends on the other.
  • For stable shared kits, pre-built XCFramework binaries skip recompilation entirely and can cut clean-build times significantly.