Skip to content
Prepare to Grow

iOS Architecture

Repository and Service Layers

Design data and capability boundaries that hide infrastructure without creating meaningless pass-through abstractions.

Advanced6 min read2 guided activities

Before you begin

What you will understand

  • Distinguish repositories, services, clients, and use cases.
  • Design domain-oriented repository contracts.
  • Coordinate remote and local data behind one boundary.
  • Avoid generic manager layers and unnecessary forwarding.
01

Learning stage

Concept Explanation

A repository presents domain data as a collection-like capability: fetch courses, observe progress, save a lesson completion. Callers ask in product language and do not need to know whether results come from HTTP, a database, memory, or a cache. A repository may coordinate several data sources and apply freshness or conflict rules.

A service represents a focused capability or operation that is not naturally collection-shaped, such as authentication, image processing, analytics, or certificate generation. A client is commonly a lower-level adapter for a protocol or system, such as an HTTP client. A use case coordinates a product workflow and may call repositories or services while applying policy.

Introduce a boundary when it hides a meaningful decision, supports replacement, or provides a stable domain vocabulary. A type that simply renames every API-client method adds indirection without isolation. Define error semantics, caching expectations, concurrency, and consistency at the boundary so callers know what the capability promises.

  • Repository: domain-oriented access to stored or remote entities.
  • Service: focused operational capability.
  • Client: adapter to a lower-level external system or protocol.
  • Use case: coordinates a user-meaningful application workflow.
  • Data source: concrete remote, persistent, or in-memory origin.
  • Cache policy: rules for freshness, fallback, and replacement.
02

Learning stage

Syntax & Examples

A repository actor owns remote-versus-cache decisions and returns domain values through one contract.

LiveCourseRepository.swiftswift
protocol CourseRepository: Sendable {
  func courses(policy: LoadPolicy) async throws -> [Course]
  func course(id: Course.ID) async throws -> Course
}

enum LoadPolicy: Sendable {
  case remoteOnly
  case cacheElseRemote
  case refreshAfterCache
}

actor LiveCourseRepository: CourseRepository {
  private let remote: any CourseRemoteDataSource
  private let cache: any CourseCache

  init(remote: any CourseRemoteDataSource, cache: any CourseCache) {
    self.remote = remote
    self.cache = cache
  }

  func courses(policy: LoadPolicy) async throws -> [Course] {
    if policy != .remoteOnly, let cached = try await cache.readCourses(), !cached.isEmpty {
      return cached
    }

    let courses = try await remote.fetchCourses()
    try await cache.replaceCourses(with: courses)
    return courses
  }

  func course(id: Course.ID) async throws -> Course {
    if let cached = try await cache.readCourse(id: id) { return cached }
    let course = try await remote.fetchCourse(id: id)
    try await cache.save(course)
    return course
  }
}
The domain-facing contract communicates load policy while transport and persistence details remain internal.
03

Learning stage

Real-World Usage

Connect the architectural concept to maintainable iOS features and real change scenarios.

  • Add offline fallback without changing presentation code.
  • Combine API responses with locally stored completion state.
  • Replace a live dependency with an in-memory repository in tests and previews.
  • Centralize freshness, retry, and conflict policies for one domain area.
04

Learning stage

Common Mistakes

Identify abstractions and dependencies that add coupling, ambiguity, or unnecessary ceremony.

  • Calling every dependency a service or manager.
  • Exposing DTOs, URLs, database records, or HTTP status codes through a domain repository.
  • Creating one repository for the entire application.
  • Adding a repository that only forwards calls and hides no decision.
  • Leaving cache freshness and source-of-truth behavior undefined.
05

Learning stage

Practice Exercises

Apply the design to a small feature and evaluate it through observable change scenarios.

Activity 1

Design a repository contract

Define a progress repository that loads course progress, records completion, and supports offline use. Document identity, errors, and consistency expectations.

Hint: Write the contract in domain terms before choosing storage technologies.

Activity 2

Coordinate cache and remote data

Implement cache-first and remote-refresh policies behind one repository, then test stale cache, network failure, and successful refresh.

Hint: Decide which result the caller receives and when the cache changes.

06

Learning stage

Knowledge Check

Answer each question before revealing the explanation.

QUESTION 1

What should a repository normally expose?

  1. A.Domain-oriented data operations independent of storage details
  2. B.Raw URLSession requests
  3. C.UIViewController navigation
  4. D.Every third-party SDK method
Reveal answer

Domain-oriented data operations independent of storage details

The repository protects callers from transport, cache, and persistence decisions.

QUESTION 2

When does a repository become unnecessary indirection?

Reveal answer

When it only renames and forwards another dependency without hiding decisions, stabilizing vocabulary, or enabling useful replacement.

Every boundary has navigation and maintenance cost, so it should earn that cost.

07

Learning stage

Interview Preparation

Explain the decision, dependency direction, trade-offs, and evidence—not only the pattern name.

How do you distinguish a repository from a service?

Short answer: A repository provides domain-oriented access to entities; a service provides a focused operation or capability.

Explore the detailed answer

I use repositories when callers need a stable collection-like interface over remote or local data, often with caching and mapping. Services fit capabilities such as authentication or analytics. Use cases coordinate workflows across them. I avoid relying on suffixes and instead define the contract, policy, and reason the boundary exists.

Possible follow-ups

  • • Where should DTO-to-domain mapping occur?
  • • Who should own cache policy?
08

Learning stage

Key Takeaways

Repositories and services are valuable when they protect domain-facing code from infrastructure decisions and communicate clear behavioral contracts.

  • Use domain vocabulary at data boundaries.
  • Repositories may coordinate multiple data sources.
  • Services represent focused capabilities.
  • Keep transport and persistence models internal.
  • Do not add a layer that hides no meaningful decision.