Async Digital / Iris
Case study

Where URL handling lives

Containment

The real test of any infrastructure library is simple: can the consumer still point to where the magic lives? If the answer is just a folder name and a low percentage of the codebase, the library is doing its job. This piece gives that answer for Iris, measured against its demo consumer.

Abstract

Every library you adopt rewrites some of your code in its image. The containment test asks how much. Deep linking is invasive by nature (URLs can land in any view state), so the answer matters.

This case study measures one consumer of Iris: a single folder with 19.2% of the lines (1,353 lines), three documented imports outside it, a domain layer with zero Iris symbols, and a tiny abstraction footprint (one protocol, one subclass, one override in the messages flow). The number that looks worst (24 direct view reads of coordinator state) turns out to be the natural cost of shared ephemeral state, and exactly what makes tap and URL behaviour identical.


Containment

The question that measures a library

Adopt any dependency and some of your code starts to look like its code. That is the deal. The question worth asking is how much, and where.

A logging library that touches one file is cheap. A navigation library that wants a delegate in every screen is expensive. Deep linking sits at the expensive end by nature because a URL can land anywhere, touching navigation, sheets, scroll, search, and more. The lazy version smears the handling across the whole app.

I hold a deep-linking library to a harder test: the consumer should answer “where does your URL handling live?” with one folder name and a low percentage.

The numbers below come from the same demo messaging app used across this series. What follows is the audit you would want before adopting anyone else’s layer.


Footprint

One folder, and an honest number

The demo app’s answer is one folder (DeepLink/) holding 19.2% of the lines (1,353 lines across 12 files). Everything Iris touches lives there. The rest of the app is free to forget Iris exists, except for three deliberate touchpoints described below.

19.2% only makes sense against what it contains. This is not a simple router. The folder implements an 18-intent surface with state-aware resolution, race-safe cancellation, last-writer arbitration, no-op elision, and full tracing. Compressing all of that into under a fifth of the app (with almost everything else untouched) is Iris doing its job.

Fig 1 Where the lines live. The DeepLink/ folder (Iris integration point) is 19.2% of the codebase; the rest of the app is free to forget Iris exists.

Boundary

Three imports and a silent domain layer

Exactly three files outside the folder import Iris: the app entry point, one destination view, and one sheet view. The entry point wires the coordinator into the environment and hands incoming URLs over. The destination view receives a handoff after a push so it can finish what the URL started. The sheet view asks for one explicit forward step. Each is small and deliberate.

The domain layer (the part that knows what conversations, messages, people, and attachments actually are) contains zero Iris symbols. No import, no reference, no leakage. Iris owns how you get somewhere; the domain owns what is there when you arrive.

Fig 2 One folder, three documented imports, and a domain layer Iris never reaches.

Boundaries like this earn their keep when things change. When a breaking release of Iris reshaped its coordinator base, the consumer absorbed the entire migration inside one file. Change on one side of a real boundary stays on that side.


Abstraction

What the consumer actually writes

Adopting Iris costs the consumer three constructs, per flow. The messages flow, this piece's subject throughout, is the measure:

Note what the override does not contain: Iris handles all structural navigation (push, present, pop, dismiss). The consumer only describes what its app should do. Iris executes it.

The step lists act as documentation of the URL surface. Opening a conversation becomes a reset prefix plus scroll + push. Every URL declares its needs once; Iris trims the prefix to match actual state. The old way (hand-written dismiss/pop/push trails) is what most apps end up with. The declarative form says “this is the destination” instead of “here are the directions.”


Parity

The pollution that isn’t

The number that looks worst is this: views read coordinator state directly 24 times across eight properties (expanded avatar, active search, pending attachment, last conversation, etc.). At first glance it looks like leakage.

Trace the writers and it flips. Every one of those properties has a non-Iris writer too: avatar taps, search field input, natural pops. The same state would exist without any deep linking (just owned differently). Iris didn’t invent the state. It put it in one place so both taps and URLs write through the same surface.

That single source is what delivers parity. The settings tap builds the URL and goes through the resolver, exactly like an external link.

No drift between tap and URL, because there is no second code path.

Full picture across the view layer: 48 callsites touch the deep-link surface. 19 are ordinary taps. 3 are deliberate URL-routed opens (settings, search, profile). 2 call navigation directly. The remaining 24 are the state reads (the normal price of shared ephemeral state, not something Iris imposes).


Limits

What the design pays for

No design is free, and a containment audit that only reports the wins isn’t an audit. Three costs are worth naming.

Caveat

The coordinator owns eight pieces of ephemeral UI state. At this scale that is manageable; at around 12 it would start to feel like a god object, and splitting navigation state from ephemeral UI state into separate observables would be the natural next move.

The function that expands intents into steps handles every intent in one place. It reads well at the current size of the URL surface; past roughly 25 intents it would want breaking into helpers per intent family.

Determinism has a test bill. Deep linking accounts for about half of the consumer’s test suite by line count, 50.3% of it. Races, cancellation, state-aware resolution, and URL parsing all need coverage, so the weight is justifiable, but a library that handed its consumers fewer load-bearing concurrency surfaces would let them write fewer tests.

Bottom line: the test is passed when the consumer can name one folder and a low percentage. Here: DeepLink/ (Iris integration) at 19.2%, clean domain, three imports, coupling that is just the normal price of shared state. Iris earns its abstraction.

Containment answers where the code lives. The harder question (what should this URL mean given current state?) is answered next by the snapshot and resolver: state-aware resolver. Full series on the hub.