I recently encountered a challenging problem at work when given the task of integrating into our iOS codebase a new authentication service called Auth0. While Auth0 reduces many of the complexities around authentication and authorization, our architecture wasn’t designed to handle token rotation prior to the app making its network requests. As with many problems, the ideal solution to this problem wasn’t obvious. In this case, I first took a step back and thought about different approaches. It’s crucial to think; use UML diagrams to help you think; learn from others. It’s important for iOS developers to be familiar with the structural tools at their disposal to solve architectural bottlenecks in a modular and extensible way as advocated by the popular book “Design Patterns,” written by Erich Gamma.

When architecting our codebase, we should adhere to the Single Responsibility Principle which states: “That each class should have one responsibility and one single purpose.” When feature requirements include cross-cutting concerns such as event logging, event tracking, and user authentication, the decorator pattern is the recommended structural tool over the misuse of singletons, which pollutes our client code and hinders our architecture flexibility.

The Problem

How can we add the ad hoc refresh token operation to our network service without introducing dangerous changes to our network service class while rotating the refresh token without producing race conditions?

The Solution

If you’re not familiar with various structural patterns, your first inclination might be to change the class’ implementation, but that means every instance will now have new behavior; we don’t want that.

Instead, we’ll use the decorator design pattern, defined here as:

… a structural design pattern that lets developers attach new behaviors to objects by placing them inside a special wrapper that contains the behaviors.

Utilizing the decorator, we are only changing the behavior of an instance (the decoratee) rather than altering the whole class, and this idea is powerful.

Every time possible, avoid adding behavior to an established class. Instead, a better approach is to wrap an instance with just the behavior it needs. The decorator not only gives us the powerful ability to add behavior non-intrusively, but also makes it easier to unit test our decorator in isolation, because it shares the same interface as the decoratee.

Image: Here’s how we would like our architecture to look:

Decorator Pattern UML

Decision tree – (Happy Path)

The decorator will decorate the instance of our network service, and be responsible for executing our token refresh and signature operation. The operation should execute in the following order:

  1. Check if the access token is valid.
    • If valid, proceed as usual.
  2. If invalid, execute a token refresh.
    • Be careful of possible race conditions.
  3. Once the token is refreshed, proceed with the network requests.

Boilerplate Code

Here is a boilerplate setup of the decorator pattern written in Swift. You can use it as a starting point for your own custom implementation.

Here’s our protocol:

protocol HttpClient {
    // methods signature
    func fetch(token: String)
}

Here’s our network service class or the decorate in this case:

final class NetworkService: HttpClient {
	// Behavior
      func fetch(token: String) {}
}

Here’s our decorator:

final class NetworkServiceDecorator {
   private let networkService: NetworkService 
   init(networkService: NetworkService) {
      self.networkService = networkService
   } 
   // Apply your new behavior
   // Call the decoratee
   func handleRefreshToken() {
      // Handle any operations here 
      // Then call the decoratee
      networkService.fetch()
   }
}

Here’s how we compose the decorator and wrap the decoratee:

let networkService = NetworkService()
let networkServiceDecorator = NetworkServiceDecorator (networkService: networkService)

Summary

Today we explored how to tackle a common problem you will undoubtedly run into when introducing new behavior into an existing codebase. The good news is that senior developers have already created elegant solutions to many of the problems you’ll face in your career. You can learn many more of these elegant solutions in the popular book “Design Patterns,” written by Erich Gamma. Overall, this problem demonstrated the importance of thinking through structural enhancement rather than solving problems locally.

That’s all, my friends. I hope this post unblocks you from any ongoing challenge you may be facing. If you have any questions or edge cases you’d like to discuss, just leave a comment below. If you want to support this blog you can Buy Me a Coffee. You can also connect with me on LinkedIn and Twitter, where I share all my new posts.