Modular App Agency

Clean Architecture and SOLID principles

Practical patterns to scale software safely.

This article explores how Clean Architecture and SOLID principles can make mobile apps easier to maintain, extend, and test.

Why clean architecture matters

When projects start small, it’s tempting to put everything in a single view controller or class. It works at first, but as soon as features grow, things get messy. Adding one more button breaks three other features, testing is painful, and onboarding new developers feels like punishment.

Clean Architecture is all about separation of concerns. Each part of the codebase has a clear responsibility, and dependencies point in the right direction (inwards). This makes it easier to swap things out, test them in isolation, and extend the app without fear.


SOLID principles in practice

The SOLID principles are five guidelines that support clean architecture. Let’s break them down with mobile app examples.

1. Single Responsibility Principle (SRP)

A class should only have one reason to change. In iOS, that means your view models shouldn’t also fetch data from the network. Instead, delegate that work to a service.

Bad:

class LoginViewModel {
    func login(email: String, password: String) {
        // also builds the request, calls API, parses JSON…
    }
}

Better:

class LoginViewModel {
    private let authService: AuthService

    init(authService: AuthService) {
        self.authService = authService
    }

    func login(email: String, password: String) async throws {
        try await authService.login(email: email, password: password)
    }
}

2. Open/Closed Principle (OCP)

Software should be open for extension, but closed for modification. Instead of editing old code whenever a new case appears, design it to be extended.

Example: payment methods. Don’t put a giant switch inside PaymentProcessor. Instead, make a protocol PaymentMethod and let new types conform.


3. Liskov Substitution Principle (LSP)

If you inherit from a class or conform to a protocol, the new type should behave without breaking expectations.

For instance, if Cache promises get(key:) always returns something or nil, a subclass shouldn’t suddenly throw an error instead.


4. Interface Segregation Principle (ISP)

Don’t force classes to implement things they don’t use. Keep protocols small and focused.

Bad:

protocol Database {
    func save()
    func delete()
    func rollback()
    func migrate()
}

Better:

protocol Saveable { func save() }
protocol Deletable { func delete() }

5. Dependency Inversion Principle (DIP)

High-level modules shouldn’t depend on low-level details. Both should depend on abstractions.

Example: instead of your ViewModel depending directly on URLSession, depend on a NetworkClient protocol. This way you can swap the real one for a mock in tests.

protocol NetworkClient {
    func request(_ url: URL) async throws -> Data
}

Pulling it together

When you combine Clean Architecture with SOLID principles:

  • Your features are modular (easy to swap or test).
  • Your dependencies flow inward, keeping the core logic isolated.
  • Your codebase stays flexible, so adding features doesn’t mean rewriting half the app.

This isn’t about overengineering from day one. It’s about setting guardrails so your project can grow safely. Even small apps benefit, because they’re easier to test and debug.


Conclusion

Clean Architecture and SOLID principles give structure and discipline to your codebase. By respecting separation of concerns and designing around abstractions, you build software that can evolve without fear. It’s like building on a strong foundation — the higher you go, the safer it feels.

Happy building 🚀