Codebreaker: Shared Contract Pattern — How Protocols Solve Cross-Team Bottlenecks
“This article is a continuation of my previous post on Shared UI vs Shared Contract here. If you haven’t read it yet, I recommend checking…
Codebreaker: Shared Contract Pattern — How Protocols Solve Cross-Team Bottlenecks
“This article is a continuation of my previous post on Shared UI vs Shared Contract **here**. If you haven’t read it yet, I recommend checking it out first to get the full context.”
In mobile engineering teams, velocity often clashes with governance. Cross-team dependencies silently kill sprints. I faced this exact problem in my previous company:
- Team C (Consumer) needed to implement a feature immediately.
- Team B (Owner) controlled the real component, which had to pass a three-week approval process.
- Team C’s desired behavior conflicted with Team B’s default behavior.
For example, the shared UI component was a ProfileCard:
- Team B: tapping the profile picture → navigate to profile screen.
- Team C: tapping the profile picture → edit profile picture (open image picker).
Naive solutions were tempting but flawed:
- Hard dependencies — Direct imports between modules create circular dependencies and technical debt.
- Premature shared components — Promoting untested UI to shared libraries spreads bugs across modules.
- Duplicating the component — Violates governance and leads to double maintenance.
So what’s the solution?
Enter the Shared Contract Pattern
The principle is simple:
- Consumer only knows the contract: the data it needs, the view it consumes, and callbacks for events.
- Owner implements the real component fully encapsulated.
- Connection is orchestrated via composition root / parent app.
Key benefits:
- Teams can develop in parallel.
- Consumers can mock dependencies for tests or early development.
- Governance rules are respected: no premature shared UI, no hard dependencies.
- Return type is UIView only, avoiding dependency leaks and module coupling.
Step 1: Define the Contract
public struct ProfileData {
public let name: String
public let pp: String
}
public protocol ProfileDelegate: AnyObject {
func onProfileImageTapped()
}
public protocol ProfileDataSource: AnyObject {
func makeProfileView(for data: ProfileData) -> UIView
var delegate: ProfileDelegate? { get set }
}
- ProfileDataSource provides a factory method to build the view.
- ProfileDelegate handles events like tapping the profile picture.
- Consumer never accesses internal UI logic.
Step 2: Owner Implementation
class RealProfileDataSource: ProfileDataSource {
public weak var delegate: ProfileDelegate?
func makeProfileView(for data: ProfileData) -> UIView {
let containerView = UIView()
containerView.backgroundColor = .systemBlue
let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
imageView.backgroundColor = .systemYellow
imageView.isUserInteractionEnabled = true
containerView.addSubview(imageView)
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap))
imageView.addGestureRecognizer(tapGesture)
return containerView
}
@objc private func handleTap() {
delegate?.onProfileImageTapped()
}
}
- UI and logic are encapsulated.
- Delegate allows consumer to override behavior safely.
Step 3: Composition Root
struct ModuleCComposer {
static func compose(navigationController: UINavigationController, useMock: Bool = false) {
let data = ProfileData(name: "Firstname Lastname", pp: "https://url-image.com")
// Decide between real or mock datasource
let dataSource: ProfileDataSource
if useMock {
dataSource = MockProfileDataSource()
} else {
dataSource = RealProfileDataSource()
}
let profileView = dataSource.makeProfileView(for: data)
let viewController = ProfileViewController(profileCard: profileView)
dataSource.delegate = viewController
navigationController.pushViewController(viewController, animated: true)
}
}
- Consumer receives UIView.
- Delegate wiring connects events to consumer.
- Mock injection allows early development or testing without relying on Team B.
Step 4: Consumer (Team C)
class ProfileViewController: UIViewController, ProfileDelegate {
let profileCard: UIView
init(profileCard: UIView) {
self.profileCard = profileCard
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) { fatalError() }
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
profileCard.frame = CGRect(x: 50, y: 100, width: 100, height: 100)
view.addSubview(profileCard)
}
func onProfileImageTapped() {
print("Consumer Logic: Open Image Picker / module-specific behavior")
}
}
- Consumer only knows view + delegate.
- All behavior is handled via protocol callback.
Step 5: Mock for Testing
class MockProfileDataSource: ProfileDataSource {
weak var delegate: ProfileDelegate?
func makeProfileView(for data: ProfileData) -> UIView {
let view = UIView()
view.backgroundColor = .systemGray
// Simulate tap event asynchronously
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
self.delegate?.onProfileImageTapped()
}
return view
}
}
- Team C can run tests or prototype without waiting for Team B.
- Fast iteration, zero dependency on production implementation.
Simulating Build-Time & Compile Impact
Assumptions:
- Consumer C: 100 LOC
- Shared Contract: 50 LOC (protocol/interface)
- Shared UI / Owner B: 150 LOC
- Parent App / Composition Root: 200 LOC
[embed]
Rough Formula:
T_total = Σ(T_build) per affected module
- Shared Contract: only Owner + Parent rebuild for internal UI.
- Shared UI: all consumers must rebuild → high idle time.
Why DataSource + Delegate?
Out of many options (closure, Combine, event bus, DI, custom callbacks), I chose DataSource + Delegate because:
- Familiar to iOS engineers — Apple uses it in UIKit/SwiftUI (UITableViewDataSource / UITableViewDelegate).
- Minimal onboarding friction — engineers don’t need to learn a new pattern.
- Clear separation of concerns — DataSource builds view, Delegate handles events.
- Mockable & testable — allows parallel development/testing.
Before vs After
Before Shared Contract:
- Team C waited 3 weeks for Team B approval.
- Hard dependencies or premature shared components caused bugs.
After Shared Contract:
- Team C can use mock or real data source immediately.
- Team B can finish internal UI without blocking consumers.
- Delegate pattern ensures safe overrides.
- Governance and velocity are preserved.
Takeaway
The Shared Contract Pattern with DataSource + Delegate transforms organizational latency into technical flexibility.
- Contracts define what can happen.
- Delegates define who handles it.
- Composition root orchestrates dependencies.
Teams can move faster, reduce risk, maintain quality, and test safely without waiting on other teams.
Interested in discussing mobile architecture? Connect with me on **LinkedIn.**
메타데이터
- post_id
- 4fb2e57834cb
- slug
- codebreaker-shared-contract-pattern-how-protocols-solve-cross-team-bottlenecks-4fb2e57834cb
- url
- https://medium.com/@solihin.chiko/codebreaker-shared-contract-pattern-how-protocols-solve-cross-team-bottlenecks-4fb2e57834cb
- canonical_url
- https://medium.com/@solihin.chiko/codebreaker-shared-contract-pattern-how-protocols-solve-cross-team-bottlenecks-4fb2e57834cb
- author_url
- https://medium.com/@solihin.chiko
- status
- ok
- fetched_at
- 2026-06-09 15:37:30