Hexagonal Architecture in Swift: Clean Logic Without Dependencies

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

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.swift


Notice: 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                

Blogs

Book Recommendations