DEV Community

Jade Silveira
Jade Silveira

Posted on • Edited on

Strategy Pattern in Swift for Modular and Scalable Architectures

Blank frame
Large applications often need to support multiple behaviors that follow the same interface but differ in implementation. This appears in many areas such as:

  • error handling
  • analytics tracking
  • network policies
  • design systems and reusable UI components

A common but problematic approach is to use enums combined with switch statements. While simple at first, this quickly leads to rigid code that is difficult to extend.

A better approach is the Strategy Pattern, which allows behavior to be encapsulated and swapped without modifying existing code.


The Problem with Enum-Based Behavior

A typical implementation might look like this:

enum ErrorType {
    case generic
    case apiError
    case connectionError
}
Enter fullscreen mode Exit fullscreen mode

Behavior is then resolved using switch:

struct ServerError {
    let errorType: ErrorType

    var title: "String {"
        switch errorType {
        case .generic: return "Generic error"
        case .apiError: return "Api error"
        case .connectionError: return "Connection error"
        }
    }

    var subtitle: "String {"
        switch errorType {
        case .generic: return "An error has occurred"
        case .apiError: return "Api failed to response"
        case .connectionError: return "Lost connection"
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

This approach introduces several issues:

  • new cases require modifying existing code
  • logic becomes centralized in large switch blocks
  • it violates the Open/Closed Principle
  • shared enums can create tight coupling between modules

Instead of selecting behavior with conditionals, we can encapsulate each behavior into its own object.


Introducing the Strategy Pattern

The Strategy Pattern defines a family of behaviors, encapsulates each one, and makes them interchangeable.

In Swift, this fits naturally with protocol-oriented programming.

protocol ServerErrorProtocol {
    var title: String { get }
    var subtitle: String { get }
}
Enter fullscreen mode Exit fullscreen mode

Each concrete implementation defines its own behavior.

struct GenericServerError: ServerErrorProtocol {
    let title = "Generic error"
    let subtitle = "An error has occurred"
}

struct ApiServerError: ServerErrorProtocol {
    let title = "Api error"
    let subtitle = "Api failed to response"
}

struct ConnectionServerError: ServerErrorProtocol {
    let title = "Connection error"
    let subtitle = "Lost connection"
}
Enter fullscreen mode Exit fullscreen mode

Usage becomes straightforward:

func displayError(_ error: ServerErrorProtocol) {
    print(error.title)
    print(error.subtitle)
}

displayError(GenericServerError())
Enter fullscreen mode Exit fullscreen mode

No switch statements are required.


Benefits

Open for Extension

Adding a new behavior only requires a new strategy:

struct TimeoutServerError: ServerErrorProtocol {
    let title = "Timeout"
    let subtitle = "The request timed out"
}
Enter fullscreen mode Exit fullscreen mode

Better Separation of Concerns

Consumers only depend on the protocol, not on concrete implementations.

Easier Testing

Strategies can easily be mocked.

struct MockServerError: ServerErrorProtocol {
    let title: String
    let subtitle: String
}
Enter fullscreen mode Exit fullscreen mode

Example test:

func testDisplayErrorUsesProvidedStrategyValues() {
    let error = MockServerError(
        title: "Mock title",
        subtitle: "Mock subtitle"
    )

    XCTAssertEqual(error.title, "Mock title")
    XCTAssertEqual(error.subtitle, "Mock subtitle")
}
Enter fullscreen mode Exit fullscreen mode

Tests become:

  • more isolated
  • easier to read
  • less coupled to implementation details

Strategy in Modular Architectures

This pattern becomes particularly useful in modular architectures.

Using shared enums forces modules to modify shared code when adding new cases. This increases coupling and can trigger unnecessary module rebuilds.

With the Strategy Pattern, each module can define its own behavior without modifying shared code.

struct PaymentDeclinedError: ServerErrorProtocol {
    let title = "Payment declined"
    let subtitle = "Your payment method was rejected"
}
Enter fullscreen mode Exit fullscreen mode

Strategy in Design Systems

The same approach can also be applied to Design Systems.

Instead of using enums to configure components (such as styles or variants), strategies allow each configuration to define its own behavior.

For example, a button component could receive a strategy describing how it should render:

protocol ButtonStyleStrategy {
    var backgroundColor: UIColor { get }
    var textColor: UIColor { get }
}
Enter fullscreen mode Exit fullscreen mode

Different styles can then be implemented independently.

This keeps the component flexible without requiring large enums or switch statements inside the UI layer.


Using Singleton Strategies (Optional)

When strategies are stateless and immutable, creating new instances repeatedly may be unnecessary.

In these cases, strategies can be exposed as shared instances.

final class GenericServerError: ServerErrorProtocol {

    static let shared = GenericServerError()

    let title = "Generic error"
    let subtitle = "An error has occurred"

    private init() {}
}
Enter fullscreen mode Exit fullscreen mode

Usage:

displayError(GenericServerError.shared)
Enter fullscreen mode Exit fullscreen mode

Benefits include:

  • avoiding repeated allocations
  • guaranteeing a single instance across the app
  • making strategies behave like reusable configuration objects

This pattern is particularly useful in design systems and shared UI configurations.


Final Thoughts

The Strategy Pattern is a simple but powerful tool for improving flexibility and modularity.

By encapsulating behavior into interchangeable objects, we can:

  • remove conditional logic
  • follow SOLID principles
  • reduce coupling between modules
  • enable scalable architectures

Combined with Swift’s protocol-oriented design, Strategy becomes a natural and elegant solution for building maintainable systems.

Top comments (0)