Why We Ditched Conventional iOS Networking

And Our Solution

John Hammerlund
The Mindbody Dev Report
7 min readMar 21, 2018

--

Before I get into this, I’ll start with a disclaimer: this isn’t your typical I-made-a-fancy-new-thing tech blog with atypical, obscure patterns. No, this is an article on taking common, basic paradigms and applying them to places we may not have thought of before — or, more accurately, in places where we forgot they were relevant.

Software products tend to go through the same phases that sum up the Capability Maturity Model, translated to software development. We usually hit the ground running and cut corners to iterate and to innovate. This is because we don’t want to waste our time and money over-architecting solutions to answers we don’t yet have. We want to focus our efforts on inventing a new thing, so we prevent any effort towards reinventing the wheel. Across all of the mobile products I’ve worked on and built, one of those wheels has always persisted: networking.

At MINDBODY, we've maintained and scaled many different mobile products for more than four years. Again, most software products share the same roots, and our mobile apps are no exception. Varying by age, every product somewhat-heavily relied on what many consider to be de facto open-source networking libraries of the times:

These are powerful, community-supported third-parties that solve the majority of networking asks of any Cocoa developer — the all-terrain tires of Cocoa HTTP libraries. However, we’re an enterprise, and to bring the analogy full-circle, can’t slap a bunch of all-terrain tires on a Peterbilt and expect to drive very far.

It’s not you, it’s us (image courtesy of Pixabay)

For years, we were continuously retrofitting these common requirements:

  1. The ability to conglomerate multiple API’s with different paradigms and transport formats into a single network session.
  2. The ability to apply different levels of authorization and other one-off decorations within a single session.
  3. The ability to have multiple sessions with different host/client configurations with the above two requirements.

Above all else, we yearned for a SOLID foundation.

Building a completely new networking layer sounds expensive, risky, and downright impractical, which heavily narrowed the gap for failure. Ultimately, this meant we needed to start with definitive answers to two questions: how do we design the new wheel and how do we slap the thing onto a moving car?

Designing the Wheel

Someone once told me, “no new architecture succeeds until after its third failure.” As a firm believer in this statement, I’m going to transition the topic and talk a little about Node.js.

Why Node.js?

No, I’m not about to start praising JavaScript and its dynamic, loosely-typed, nested-callback glory. The reason Node.js became so popular is ease and simplicity , often with a cost. But in some cases, the language and platform provide an infrastructure for great patterns to shine. In our case, we decided to entertain Express.js and its implementation of the middleware pattern.

Roughly how Express.js processes incoming HTTP requests

No fancy global request managers riddled with routing rules, serialization patterns, global request headers, oversimplified embedded authorization mechanics, cache policies, etc.; no request operations that absorb too many responsibilities to try and swing the pendulum too far in the modular direction; and no need to spend three hours reading the documentation on how it all works.

The Network Pipeline Architecture

When simplified into architecture, we found there are four main players for any given request:

  • Pipeline
  • Collection of middleware
  • Request
  • Response

In our Swift implementation, a session is synonymous with a request pipeline. Any changes exhibited or global behaviors defined within the pipeline will affect all requests within that session. A session client manages a single pipeline with a provided URLSessionConfiguration, and it directly accepts URLRequests for input. This means that the networking architecture is a very thin layer on top of Apple’s networking libraries. The client simply funnels requests through a serial queue, processes them through a provided chain of middleware (can be different per-request), and finally transmits them to the URLSession. This fulfills our first requirement.

The Network Pipeline Architecture, in our Swift implementation

The Middleware Paradigm

Middleware components have scoped responsibilities based on concrete implementations, but they all can do three things:

  1. Modify the request
  2. Freeze/empty the outgoing pipeline
  3. Short-circuit the response with an error

Middleware is defined as a simple protocol, and custom middleware can be defined and injected from anywhere. To put this system to the test, we needed to prove that middleware can be more than simple filter-processing; it needed to support an entire subsystem.

To prove this, we built and injected the first piece of middleware completely outside of the networking library: a system that fully implements the OAuth2 Authorization protocol (RFC 6749 and RFC 6750). This fulfills our second requirement.

A high-level view of how Auth middleware processes requests

To recap: session == Network Pipeline

We can have multiple network pipelines operating alongside each other. Additionally, we can inject middleware both locally (i.e. per-request) or globally (per-session). In our case, this means that authorization middleware can be created, tweaked, or omitted completely for each individual request in a given session. This fulfilled our last requirement.

So, we’ve designed the wheel, and at this point, we’ve battle-tested it. The last standing question is how to slap that sucker onto a moving car.

How to Inject Dependency Injection (What?)

You read that right, dependency injection injection (inception?). We’ve officially moved from talking about how to rebuild a networking layer to how to replace a four-year-old networking layer completely. Cue Hans Zimmer.

As it turns out, this wasn’t as easy as replacing a dependency layer. Considering this is very often one of the first pieces that you build in any network-dependent application, and also considering that you don’t want to waste time over-architecting in early stages, the layer almost seemed inseparable in most of our applications. From these truths, I came up with a two-phase approach:

  1. Stop the bleeding
  2. Dependency Injection-jection

Stop the Bleeding

The goal of the first phase: allow the old and new networking code to coexist. Achieving this goal would allow developers to build purely on the new network pipeline for future development without negatively impacting existing API calls implemented against the legacy networking layer.

From this angle, we saw the originally projected work diminish astronomically. Across three different networking layers in three different mobile apps, it turned out there was only one problem to solve in each different implementation, which was token management.

Dependency Injection-Jection

Alright, I’ll stop with the punny names. The next and final goal was to fully cut ties with the old dependency. It’s any enterprise developer’s dream to finally remove AFNetworking from the Podfile.

Of course, in most situations involving mobile development, we’re talking time and money. To keep the migration practical, the second phase walks the line between cost and quality. Rather than trying to reconstruct our entire legacy transport layer to point to the new architecture, we took a more hands-on approach.

First and foremost, we identified exactly the touchpoints between the application and the given dependencies within the transport layer — the “utilized interface.” I found the quickest way to draw out the utilized interface is to start with an adapter, as such:

Since AFNetworking is an Objective-C library, we can get away with leveraging compatibility aliases / macros to reroute our dependencies:

The compiler will pretty quickly help us identify what signatures are missing from the adapter:

The compiler gave up at 129…

Once we flush out all of the required signatures, we go back to the drawing board and answer these questions:

  • How do we adapt the required legacy methods to pipe into our new architecture?
  • How will we exhibit all of the same required behaviors within the legacy networking layer?

For three massive iOS apps running on completely different networking stacks, the answers to these problems eventually became feasible. Injecting adapters allowed us to swap out networking layers with a single dependency switch.

And that was it — apparently, it was possible to remove those worn-down all-terrain tires while charging down the freeway. Even in the fast-paced enterprise world, it’s fully possible to tackle these problems, so long as they’re taken in bite-sized pieces.

Open Source

This framework is public! Check out Conduit on Github: https://github.com/mindbody/Conduit

We’re Hiring!

Information created by third parties that we may link out to or feature on our site is not endorsed by us and remains the responsibility of such third parties. MINDBODY assumes no responsibility for errors or omissions in the content.

--

--