CQRS and Event Sourcing in Swift: Architecture for Complex Projects

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

Why Traditional Architecture Fails to Handle Modern Tasks?

\n\n

Have you ever encountered a situation where simply adding a new feature to a Swift app turns into a nightmare? Bugs keep popping up, data gets lost, and the logic grows to incredible proportions? Sound familiar? Welcome to the world where CQRS and Event Sourcing become your best friends.

\n\n

These patterns are not just buzzwords. They are a way of thinking that fundamentally changes the approach to building applications in Swift. And today we will break them down using real examples. No fluff, no abstruse theories — only practice.

\n\n

What is CQRS? Divide and Conquer

\n\n

CQRS (Command Query Responsibility Segregation) is a principle that says: "Stop mixing reads and writes in the same class." It sounds simple, but the consequences are enormous.

\n\n

In classic CRUD (Create, Read, Update, Delete) architecture, you use one model for everything. For example, User. It is saved, read, and updated. But what if reading requires one set of fields and writing requires another? Workarounds begin: optional fields, flags, mappings...

\n\n

CQRS proposes separating responsibilities:

\n
    \n
  • Commands — change state. They mutate data but return nothing.
  • \n
  • Queries — read data. They do not change state, only return a result.
  • \n
\n\n

In Swift, this might look like this:

\n\n
// Command — mutation only\nstruct CreateUserCommand {\n    let id: UUID\n    let name: String\n    let email: String\n}\n\n// Query — read only\nstruct GetUserQuery {\n    let userId: UUID\n}\n\n// Command handler\nclass CreateUserCommandHandler {\n    func handle(_ command: CreateUserCommand) {\n        // User creation logic\n        print("User created: \\(command.name)")\n    }\n}\n\n// Query handler\nclass GetUserQueryHandler {\n    func handle(_ query: GetUserQuery) -> User? {\n        // User retrieval logic\n        return User(id: query.userId, name: "Alexey", email: "alex@example.com")\n    }\n}
\n\n

See? No mixing. Each class does exactly one thing. This simplifies testing, maintenance, and scaling.

\n\n

Event Sourcing: Storing History, Not State

\n\n

Now let's move on to Event Sourcing — a pattern that stores not the current state of an object, but the entire sequence of events that led to it.

\n\n

Imagine a bank account. Instead of storing the balance (1000 rubles), you store a list of events: "Account opened", "Deposit of 500", "Withdrawal of 200", "Deposit of 700". To find out the current balance, you simply replay all the events. This provides incredible capabilities: auditing, rollback of changes, analytics.

\n\n

Let's implement this in Swift:

\n\n
// Event\nprotocol Event {}\n\nstruct AccountCreated: Event {\n    let accountId: UUID\n    let initialBalance: Double\n}\n\nstruct MoneyDeposited: Event {\n    let accountId: UUID\n    let amount: Double\n}\n\nstruct MoneyWithdrawn: Event {\n    let accountId: UUID\n    let amount: Double\n}\n\n// Aggregate — restores state from events\nclass BankAccount {\n    private(set) var balance: Double = 0\n    private(set) var isActive: Bool = false\n    private(set) var accountId: UUID?\n\n    func apply(_ event: Event) {\n        switch event {\n        case let created as AccountCreated:\n            accountId = created.accountId\n            balance = created.initialBalance\n            isActive = true\n        case let deposited as MoneyDeposited:\n            balance += deposited.amount\n        case let withdrawn as MoneyWithdrawn:\n            balance -= withdrawn.amount\n        default:\n            break\n        }\n    }\n\n    // Restoration from event history\n    static func load(from history: [Event]) -> BankAccount {\n        l                

Blogs

Book Recommendations