Jacob’s Tech Tavern

Jacob’s Tech Tavern

Advanced Swift Actors: Re-entrancy & Interleaving

Understand actor behaviour with a real-world use case

Jacob Bartlett's avatar
Jacob Bartlett
Oct 10, 2023
From a Reddit post

Actors can be tricky to wrap your head around. 

Conceptually, they’re pretty simple: actors are reference-typed entities that enforce serial access to their methods and state. However, without understanding their nuances, it’s hard to know whether an actor is the right tool for the job, or if you’re just adding complexity. 

To help nurture your instinct for when to take an actor out of your toolbelt, I’m going to guide us through a use case that affects almost all apps: Authentication.

I’ll first explain the challenges of building an authentication service in your iOS app. We will use sequence diagrams to design exactly how we want our ideal system to behave. Next, I’ll briefly cover core Swift concurrency concepts, explain how actors work under the hood, and discuss what is meant by re-entrancy. Finally, I’ll bring these concepts together and demonstrate how we can use our knowledge of actors to implement an optimal authentication service in your iOS app.

If you want to code along, download the finished sample project here.

Building an Authentication System

Most mobile apps authenticate using the OAuth 2.0 standard:

  • Your users access resources on the server using an access token (sometimes called a bearer token).

  • This token is placed on URL request Authorization headers.

  • The token expires after some time, and a refresh token is used to get a new set of valid tokens. 

When your access token expires — after either a marathon user session or, more likely, when your user comes back the next day — your app needs to send a request to your backend authentication service to re-authenticate and get fresh credentials. 

We’re going to look this refresh in-depth in our example.

Our simple sample app

Our sample app is a nascent social network, and couldn’t be more simple. We’re going to release an MVP with a single feature: fetch the user’s friends from the backend and display them in a list.

Basic social app folder structure and UI

The structure is quite basic: the template ContentView, a corresponding view model, and a service to fetch the user’s friends. 

This FriendService fetches a valid auth token from the authentication service and makes the authenticated API call to our backend.

The AuthService has a dead-simple interface, with just one async method to retrieve the bearer token, which can then be added to our network request headers to authenticate them:

protocol AuthService {
    
    /// Return a valid, refreshed, bearer token
    ///
    func getBearerToken() async throws -> String
}

Our auth service is really doing a lot of work under the hood to fetch this: 

  • Fetching credentials from the Keychain.

  • Checking whether they are expired.

  • If so, contacting our backend to refresh the credentials.

  • Storing fresh credentials in the Keychain anew.

  • Finally, returning the valid token to the friend service.

We can visualise the way our services are interacting by drawing a sequence diagram:

The friend service requests a bearer token, the auth service refreshes credentials, a fresh token is returned, and the friend service makes a successful API call

Your access token and refresh token are usually stored safely in the iOS secure enclave through the iOS Keychain. To keep the example simple, I’m leaving off any Keychain operations and treating our backend services as single monolith.

This is nice and straightforward. It looks like the launch of the next big social app will go off without a hitch! 

Wait a minute.

Our product manager just called me into a meeting.

…Bad news. 

Our CEO heard on a podcast that personalisation is the new big trend, and therefore we’re going to need to introduce a personal greeting to the user when they open the app.

I love scope creep!

Our complex sample app

Here’s the new structure we require, complete with designs:

Slightly more complex structure of our app, with a personalised greeting to the user and list of their friends

The new design still has a single view and view model, but now calls two services:

  • FriendService fetches an array of [Friend]s to display to the user as a list of names.

  • UserService fetches the User in order to greet the user with their name when they enter the app.

Both of these fetches rely on making authenticated API calls — so both need to fetch a valid bearer token from our AuthService.

The problematic real-world

Life becomes tricky when you have multiple simultaneous API requests. 

In reality, most apps will be fetching data from several sources after launch, so if you aren’t careful, you’ll encounter a situation where your auth service is called multiple times unnecessarily. 

Your app only needs one single valid auth token, and so concurrent refreshes is both hugely wasteful and a potential wellspring of nasty bugs.

Here’s the sequence diagram for a naïve first-pass implementation of this more realistic situation:

The friend service and user service both simultaneously request a bearer token, the auth service refreshes credentials twice, a fresh token is returned twice, and the services make API calls with undefined results

This is the world the single-threaded JavaScript crowd want you to live in. And we complain about the 30% Apple tax!

The problem here happens when the user’s access token is expired. Since UserService and FriendService simultaneously request a valid access token, the AuthService will make two separate requests to refresh the credentials.

At best, this puts unnecessary load on our backend systems. At worst, we don’t know what will happen: we could be invalidating the new auth token by immediately refreshing again, or the second request might hit the backend rate limit with a 429 error and just fail.

This diagram doesn’t even show all the duplicate work: we aren’t just doubling-up on the refresh credentials call to the backend — wrapped in the auth service itself, we’re also duplicating a check to the Keychain, logic to check token expiry, and saving refreshed credentials back to the Keychain. 

In our ideal system, concurrent network requests should all suspend execution and wait for the token to return from the original network request — that is, they are all waiting for the same token.

Let’s diagram out this dreamy scenario:

The friend service and user service both simultaneously request an auth token, the auth service refreshes credentials only once, returns the valid bearer token to both services, and the services make successful API calls

Sounds complicated? 

Fear not — Actors are here to save the day.


How actors work under the hood 

Before we implement our dreamy optimal auth service, it would help to understand the Swift concurrency model in more detail, and how this informs the behaviour of actors.

Multi-threading

Multi-threading allows work to be done concurrently, which can enable higher throughput and performance in your app. However, it isn’t a magic bullet. Implementing multi-threading without due care can lead to issues such as data races, deadlocks, and invalid application states (and subsequent crashes).

Threads themselves also incur memory overhead (512kB by default), may take many milliseconds to instantiate, and CPU cores can lose both time and L1-cache memory when they switch between threads. Swift concurrency therefore aims to get as close as possible to the optimal scenario of 1-thread-per-CPU-core. 

Cooperative Threading 

Swift uses a Cooperative Threading Model which essentially allows different “units of work” to run on the same thread. 

When we use the await keyword, work can be suspended and make space for other work to be done on the same thread. This is a huge performance boost compared to an expensive CPU context-switch between threads.

Swift concurrency introduces continuations as lightweight objects which track which work is to be resumed. This concept is analagous to the DispatchQueue abstraction introduced in Grand Central Dispatch — a cheap and lightweight abstraction compared to expensive and memory-heavy threads.

Executors

Swift concurrency introduced the concept of Executors as a runtime feature of Swift to schedule work. This is comparable to the global system-aware thread pool introduced by Grand Central Dispatch — ensuring the low-level thread management is handled by the system rather than by application developers.

Actors each have a SerialExecutor, which we can think of a bit like a serial queue. Any work done by an actor (such as a method call) gets queued up on its serial executor.

This allows actors to express a “single threaded illusion” backed by the Swift runtime — work isolated on an instance of an actor will never execute concurrently.

Subscribe for free to continue reading

Enter your email and get every post from Jacob’s Tech Tavern.
Already have an account? Sign in
© 2026 Jacob Bartlett · Privacy ∙ Terms ∙ Collection notice
Start your SubstackGet the app
Substack is the home for great culture