April 27, 2026
Published by Mino Rakotolehibe
Every iOS app starts the same way: one target, one folder, one place for everything. Developers move fast, ship features, and the codebase grows. For a while, that’s fine. The friction is quiet at first. Then, you make a one-line fix and watch the compiler rebuild the entire app. You search for a utility someone wrote before and find three versions scattered across different folders. Before long, it’s everywhere. This is the journey of how we modularised Blip - what was there before, what changed and what we learned.
Blip is BrightHR's time and attendance app. It lets employees clock in and out using a QR code or geo-fencing, and gives managers a real-time view of who's in. It's not a standalone product. It lives inside the BrightHR ecosystem, sharing authentication, design, and infrastructure with the wider suite. Serving multiple territories.
Blip started as a single Xcode target. All the code (UI, business logic, networking, and data) lived together in one place, organised by feature folders. There were no packages, no enforced boundaries. Just folders. The architecture was MVVM with UIKit. Each screen had a ViewController responsible for rendering the UI, backed by a ViewModel that handled presentation logic, and an Interactor that owned the business logic and called into services to fetch data.
So, what made it a monolith?
Three things:
Single target: everything compiled together as one unit. One change anywhere could break anything.
No enforced boundaries: any file could import and use any other. The compiler had no opinion on this. Nothing stopped it.
Single dependency graph: all dependencies came through CocoaPods into one shared project. Every target had access to everything.

The folder structure gave the illusion of separation. But folders are just organisation — they don't enforce anything. The monolith wasn't about how the code was arranged; it was about the fact that nothing could stop one part of the app from depending on another. And as the app grew, that became a problem.
It wasn’t one thing. It was a pile of inconvenience that quietly built up until it couldn’t be ignored.
The modular structure lives in Blip package, an internal Swift package. The philosophy was layered: lower-level modules know nothing about higher-level ones and the dependency graph flows in one direction.

There are four layers.
Foundation. The base everything is built on. It contains the core types the rest of the app reasons about: Employee, Shift, UserProfile. It also owns the HTTP layer – the network client used across the app.
Infrastructure. Modules that sit above Foundation but below features. They provide cross-cutting capabilities that almost every feature needs: analytics for event tracking for example.
Shared. This is where common UI components, extension and utilities live.
Feature. The top of the stack and the user facing layer. Each module represents a self-contained product area: History, Who’s In, Adjustment requests, etc. It owns its own logic, UI, and assets.
Along the way, while we modularised the app, we also took the opportunity to update our tech stack, adopting SwiftUI and TCA.
| Monolith | Modular |
|---|---|
| Everything lives in one build target — History, Adjustment Requests, Home, Settings all compiled together | Features live in separate modules — History, WhosIn are all isolated |
| Features could reach into each other freely — an interactor in History could import a class from Home with nothing stopping it | Cannot reach into each other — the compiler physically prevents it |
| Shared code had no formal home — helpers and utilities scattered across Shared classes, with no enforced boundary | Shared code has a designated home — SharedResources is the one place for cross-feature components |
| One test suite (blipTests) covering everything — slow, no isolation | Each module has its own test target — test only what changed, in seconds |
| One dependency graph — a change anywhere could trigger a full recompile | Incremental compilation — change History, only History recompiles |
| No explicit public API — every type was visible to every other type by default | Explicit public surfaces — internal types stay internal; the module's API is a conscious decision |
Modularising didn’t just change where files lived. It changed what the compiler knew, and how fast it could respond.
Registered Office: Bright HR Limited, The Peninsula, Victoria Place, Manchester, M4 4FB. Registered in England and Wales No: 9283467. Tel: 0844 892 3928. I Copyright © 2024 BrightHR