When MVVM and MVC Stop Saving You, and Tests Turn into a Nightmare
Have you ever written a Swift app where, after six months, adding one feature broke three others, and you had to rewrite all your tests because the database changed from Core Data to Realm? If so, you're not alone. Classic layered architectures (MVC, MVVM) work great at the start but turn into a mess as the project grows.
I'll show you Hexagonal Architecture (also known as Ports & Adapters) — an approach that came from the Java and DDD world but fits Swift perfectly. We'll break it down with real examples, without abstract "User" and "Manager" classes. You'll see how to separate business logic from the database, UI, and network so you can change them without touching the application core.
This isn't just theory — it's practical knowledge you can apply to your project today.
What is Hexagonal Architecture? In Simple Terms
Imagine a hexagon (hence the name). In the center is your pure business logic (the domain). On the edges are ports (interfaces) through which the outside world communicates with the core. Adapters are the concrete implementations: database, API, UI, file system.
The main rule: the core knows nothing about the outside world. Not about UIKit, not about URLSession, not about Core Data. It only knows about its interfaces (ports).
A real-life example: imagine an online store. The core knows there is a "Cart" and "Checkout." It doesn't care where the data comes from — a web socket, a local JSON file, or a keyboard. Adapters connect to the ports and translate external signals into domain commands.
Why Should You Use This in Swift Projects?
- Testability — you test the domain without a real database or network. Just swap adapters with mocks.
- Flexibility — want to switch from Firebase to your own server? Change one adapter, not half the app.
- Readability — business logic isn't scattered across ViewControllers. It lives in one place.
- Framework Independence — your core doesn't import UIKit, Combine, or Alamofire.
Project Structure with Hexagonal Architecture in Swift
Let's jump straight to the code. Suppose we're building a task management app (Todo list). Here's what the folder structure will look like:
App/├── Domain/│ ├── Entity/│ │ └── Task.swift│ ├── UseCase/│ │ └── CreateTaskUseCase.swift│ └── Port/│ ├── TaskRepository.swift│ └── TaskOutputPort.swift├── Adapter/│ ├── Persistence/│ │ └── CoreDataTaskRepository.swift│ ├── Network/│ │ └── APITaskRepository.swift│ └── Presentation/│ └── TaskListViewModel.swift└── Main/ └── AppDelegate.swiftNotice: Domain imports nothing but Foundation. Adapter can import CoreData, UIKit, Alamofire — anything.
Step 1: Domain Entities and Ports
Let's start with the most important part — the domain model. This is a struct or enum that describes a business entity. No NSManagedObject, no Codable.
// Domain/Entity/Task.swiftstruct Task { let id: UUID var title: String var isCompleted: Bool let createdAt: Date}Now let's define the repository port (output port). This is a protocol that describes how we save and load tasks.
// Domain/Port/TaskRepository.swiftprotocol TaskRepository { func fetchAll() async throws -> [Task] func save(task: Task) async throws func delete(taskID: UUID) async throws}And the input port — the way the outside world (UI, API) interacts with the core. This is usually a UseCase.
// Domain/Us