Building scalable backend apps in Swift - The.Swift.Dev.
8/16/26 12:00 PM<br>· 20 min read<br>· featured
Building scalable backend apps in Swift
Build scalable Swift backends with onion, hexagonal, and clean architecture using use cases, repositories, transactions, and database abstractions.
Microservices, monoliths, clean architecture, MVC, MVP, MVVM, VIPER, SOLID, DRY… we have so many ways to build apps. Wouldn’t it be simpler to have massive view controllers everywhere? You could do that, and it might give you a false sense of simplicity, but putting everything into views, controllers, or models does not create a scalable architecture. On the other hand, we could talk about VIPER, which requires much more boilerplate and ceremony. However, every feature follows the same structure, so no matter what you’re building, you follow the same rules. Complex? Sure. Scalable? Yes. I’ve worked on many apps and seen many architectures, so let’s point one thing out directly: there’s no silver bullet, only a preferred way of doing things.<br>In this post, I’ll show you what I’ve learned over the last few years and how I’m approaching backend development with server-side Swift. Let’s get started. 😊<br>Hexagonal / onion / clean architecture for Swift backends<br>The onion architecture is a software design pattern created by Jeffrey Palermo in 2008. It separates an application into three main layers: domain, application, and infrastructure.<br>Hexagonal architecture , also known as the ports and adapters pattern, was proposed by Alistair Cockburn. It divides a system into loosely coupled components connected through ports, which define abstract APIs implemented by concrete adapters; in Swift, we call these protocols and their implementations.<br>Robert C. Martin’s clean architecture combines ideas from onion architecture, hexagonal architecture, and other patterns. The business domain stays at the center, with dependencies always pointing inward. The core should not depend on databases, user interfaces, or external frameworks.<br>I’ve adapted these architectures and made a few minor changes, but based on my understanding, this is how I separate concerns into layers using Swift.<br>Domain<br>The domain contains domain models. These are not database table rows, transfer objects, or API representations. They encapsulate business rules and enforce invariants. What does this sentence actually mean?<br>Business rules are the core domain rules and decisions that define how an application behaves. 😇<br>Let’s look at the following example, where we’ll model an imaginary blog engine.<br>Domain models<br>When publishing a blog post, we should always have associated tag identifiers. These identifiers are stored in a cross-reference database table (post_id, tag_id). Since we want to create a post together with its associated tags, this looks like a good business rule to model in the domain layer:<br>public struct Post {<br>public let id: String<br>public private(set) var title: String<br>public private(set) var content: String<br>public private(set) var tagIds: [String]<br>Notice that id is the only immutable property. The other properties can be changed internally, but not by external consumers. We’ll get back to this in a minute. 🕥<br>Another business rule could enforce a blog post title that is at least 3 and at most 255 characters long. This validation logic belongs in the domain layer, and a domain error can be thrown when validation fails. The domain layer should not use external dependencies, apart from Foundation or a very small shared domain library. Framework dependencies should be kept to a minimum. It is also useful to define typed throws and custom errors for the issues that end users and framework consumers can encounter when calling this layer:<br>extension Post {
public enum Error: Swift.Error {<br>case titleTooShort<br>case titleTooLong
private static func validate(<br>title: String<br>) throws(Self.Error) {<br>guard !title.isEmpty else {<br>throw .titleTooShort<br>guard title.count So how should we handle insertion? Should we make the post ID optional? I don’t think so. Vapor 4 did this with Fluent models, but a Fluent model is not a true domain model, and the terminology around repositories is mixed there as well. Fluent’s models and repositories are database access layer components. Anyway, back to the topic.<br>Instead of making identifiers optional, we could introduce a New nested struct containing the same fields without an ID. Alternatively, we could create a base object to avoid duplicating properties. For the sake of simplicity, let’s duplicate them here. AI can generate boilerplate extremely well, so why not? 🤖<br>We’ll also introduce a public static builder because we don’t want consumers to create these objects without enforcing the validation rules. This lets us create a validated New post value.<br>public struct Post {
public struct New {<br>public let title: String<br>public let content: String<br>public let tagIds: [String]
public static func create(<br>title: String,<br>content:...