Coinlocally
A live cryptocurrency exchange's real-time trading path ran on a third-party WebSocket library and locking code that three different screens each reimplemented on their own. I replaced it with an owned layer, live, with no rewrite and no feature freeze.
- Role
- iOS Engineer
- Platform
- iOS — UIKit migrating to SwiftUI
- Stack
- Swift · Objective-C interop · URLSessionWebSocketTask · Swift actors
- Scale
- 85% → 98% crash-free · Nov 2023 – Dec 2024
Borrowed pieces, on the highest-stakes path.
Coinlocally is a cryptocurrency exchange with a live iOS trading app. When I joined the iOS team, its real-time trading path leaned on borrowed pieces: a third-party WebSocket library wrapped in a thin manager class, a price chart rendered inside a web view loading a JavaScript charting widget, and order-book state that the futures screen, the spot screen, and the home screen each synchronized independently, with no shared store between them. Working alongside the rest of the team, my focus was the real-time layer: making it reliable and owned by the app itself, without ever taking the exchange offline to do it.
The problem
The app had real production reliability problems: memory leaks and race conditions were holding the crash-free session rate at 85%, concentrated in the screens handling live market data. The cause wasn't one bug. Each trading screen maintained its own dispatch queues and locks for conceptually the same kind of state, with no consistent rule for which queues were actually safe to write from concurrently, so fixing a race condition on one screen did nothing for the identical class of bug on another. On a trading app, a crash is an incident that happens mid-trade, and the exchange couldn't stop trading to be fixed. No rewrite, no feature freeze: the app had to get more reliable while it kept shipping.
Constraints
- Real money, real positions. Instability here means a user mid-trade losing a connection or a screen.
- No rewrite, no feature freeze. Modernization had to happen underneath continuous feature releases, not instead of them.
- Duplicated concurrency code, not one bug. Order-book and price-list state was synchronized independently on at least three screens, each with its own queues and locks, and no shared rule between them.
- Objective-C and Swift, UIKit and SwiftUI, coexisting. Legacy and new code had to interoperate correctly for the entire migration, from the first release to the last.
- Real-time market data under high-frequency updates. The order book and price feed can update many times a second, on the exact screens users are actively trading from.
One writer, no matter how fast the feed.
An owned WebSocket client replaced the third-party wrapper and feeds a single, concurrency-safe market-data store. Everything downstream, the order book, the chart, legacy and modern screens alike, reads from that one place instead of keeping its own copy.
A price tick arrives over the exchange's WebSocket feed. The client, written in-house to replace the inherited third-party wrapper, decodes it and writes it into the market-data store. That wrapper's own connection state had been mutated directly from delegate callbacks, with safety only enforced one layer up, differently, on every screen that consumed it. The replacement store started life protected by a serial dispatch queue with a write barrier, so concurrent reads could never race a write, and once Swift's actor model was viable, it moved into a custom actor, retiring the queue-and-barrier pattern for good. The order book and chart, both rebuilt natively, read from that one store and render every update, whether the screen showing them is still UIKit or has already moved to SwiftUI.
Own the path that fails the most.
Replace the WebSocket library, not patch it
The real-time client had been a third-party socket library wrapped in a thin manager class, with no internal synchronization of its own. It was rewritten from scratch in pure Swift, version-gated between a URLSession-based approach and URLSessionWebSocketTask, so connection state and decoding could finally live behind one boundary instead of being patched differently on every screen that touched it.
One store, not three copies of the same fix
Order-book and price-list state had been synchronized independently on the futures, spot, and home screens, each with its own queues and locks. Consolidating it into a single market-data store meant a concurrency bug got fixed once, not rediscovered on the next screen that needed the same data.
A write barrier first, an actor second, on the store that mattered
The new store was protected immediately with a concurrent-read/serial-write dispatch queue using a barrier on writes; that closed the worst race conditions right away. An earlier, narrower attempt at adopting actors had isolated a small REST-pagination buffer, not the market-data path actually driving the crashes. Once actors were viable for the real bottleneck, the store was refactored into one, replacing hand-rolled synchronization with a language-level guarantee where it counted.
Objective-C to Swift, screen by screen
The legacy codebase was migrated incrementally, alongside continuous feature releases, rather than as a branch-and-merge rewrite. The exchange never stopped trading during the migration.
Native chart and order book, replacing a web view and a vendor pod
The price chart had been a WKWebView loading a JavaScript charting widget; the order book was screen-specific UIKit composition with no shared component behind it. Both were rebuilt as native, shared views reading from the same store, for control over rendering performance under the high-frequency updates a trading screen actually sees.
What owning the path cost.
An in-house WebSocket client over the inherited library
Full control over reconnection and decoding, tuned to a high-frequency trading feed, with no vendor dependency on the most latency-sensitive part of the app.
Every edge case the library had already handled, reconnection storms, malformed frames, backpressure, became something to own directly.
One consolidated store over three independent fixes
A concurrency bug in the market-data path now has exactly one place it can live, and one place to fix it.
Every screen that used to own its state directly had to be migrated to read from a shared store instead, on a live app, without a maintenance window to do it in.
Incremental migration over a rewrite
The exchange never stopped trading, and reliability improved while the app kept shipping features the whole time.
Months of maintaining two idioms side by side: Objective-C and Swift, UIKit and SwiftUI. That's slower and messier than a clean-room rewrite looks on a slide.
Where the old pattern hid.
The same bug, fixed three times
Because the futures, spot, and home screens each declared their own queues and locks for equivalent state, some of those queues turned out to be inconsistently configured: a write barrier on a queue that was already serial does nothing, it's a no-op flag, so a screen could look protected and not be. Finding that meant auditing every screen's synchronization individually rather than trusting that a fix on one implied safety on another.
Order book consistency under bursts
The order book has to stay correct while updates can arrive many times a second. The new WebSocket client and the actor-isolated market-data store exist specifically so a burst of ticks can't produce a torn or inconsistent read on the screen a user is trading from.
Changing the concurrency model without stopping the app
Moving the market-data store from a queue-and-barrier pattern to a Swift actor meant re-auditing every place that touched it, on screens users were actively trading from, with no feature freeze to do it safely offline.
Two idioms, one app, the whole time
For a real stretch of the project, any given screen could be legacy Objective-C/UIKit or modern Swift/SwiftUI, and the app had to behave identically either way while the migration continued underneath it.
The number that mattered.
Where this work sat.
Account security, custody, and matching-engine integrity on Coinlocally lived primarily on the backend and in platform-level trading safeguards, outside this iOS client's scope. On the client, the highest-consequence work was reliability of the real-time data path itself: a wrong or torn read in a live order book is its own kind of user-facing risk on a trading app, which is the specific failure mode the actor-isolated data store exists to prevent.
Thirteen months, no downtime to work with.
Joined a live exchange, still Objective-C and UIKit
Started as iOS engineer on a production trading app with a crash-free session rate stuck at 85%, and a real-time path built on a third-party WebSocket library and a web-view chart.
Traced the crashes to duplicated locking, not one bug
The futures, spot, and home screens each synchronized market-data state independently, with an inconsistency in how their queues were configured that made some of the existing protection a no-op.
Write barrier closes the first wave
Replaced the ad-hoc, per-screen threading around market-data state with a serial dispatch queue and a write barrier in a single consolidated store, an immediate fix for the worst of the race conditions.
In-house WebSocket client, order book, and chart
Replaced the third-party WebSocket wrapper with a client written from scratch in pure Swift, version-gated across iOS releases, and rebuilt the order book and chart natively on top of it, retiring the inherited web-view widget.
Objective-C and UIKit migrate out, screen by screen
Continued the incremental move to Swift and SwiftUI, alongside continuous feature releases, with no branch-and-merge rewrite.
The store moves to a Swift actor
The consolidated market-data store was refactored from the queue-and-barrier pattern into a custom actor, correcting an earlier, narrower attempt that had put the codebase's only actor on an unrelated buffer instead of the real bottleneck. Crash-free sessions reached 98% by the end of the engagement.
What a live trading app teaches.
- Duplicated fixes aren't fixes. Three screens each protecting the "same" state independently means three chances for the protection to be subtly wrong, and it was.
- An actor in the codebase isn't the same as an actor on the problem. The first attempt at adopting Swift's concurrency model isolated the wrong piece of state; recognizing that mattered more than the adoption itself.
- Reliability work doesn't get a maintenance window. On a live exchange, it ships in the same cadence as features, or it doesn't ship at all.
More reliable, and it never closed.
Coinlocally's crash-free session rate rose from 85% to 98% over about thirteen months, without a rewrite and without a feature freeze. The app kept shipping and kept trading the entire time, built by an iOS team where the real-time layer was my piece of it. The market-data path went from borrowed pieces, a third-party socket library, a web-view chart, and duplicated per-screen locking, to one owned layer: an in-house WebSocket client and order-book renderer sitting on a single actor-isolated store. It became some of the most deliberately engineered code in the app, because it was also the most failure-prone.