← Back to list

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…

Solihin Chiko · 2025-10-01 01:41 · 2 claps · 3.3 min read
#ios-development #mobile-architecture #design-patterns #cross-team-collaboration #developer-productivity
Open on Medium ↗
Wiki topics: 📱 · Mobile Development ⏱️ · Productivity 🏛️ · Architecture 📊 · Economic Policy

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:

  1. Hard dependencies — Direct imports between modules create circular dependencies and technical debt.
  2. Premature shared components — Promoting untested UI to shared libraries spreads bugs across modules.
  3. 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:

  1. Familiar to iOS engineers — Apple uses it in UIKit/SwiftUI (UITableViewDataSource / UITableViewDelegate).
  2. Minimal onboarding friction — engineers don’t need to learn a new pattern.
  3. Clear separation of concerns — DataSource builds view, Delegate handles events.
  4. 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