- Published on
Modern Isn't a Value. Fit Is.
- Authors

- Name
- Omar Elsayed
A few weeks ago, I sat down to wrap an SDK in our codebase. The SDK uses delegates. The codebase that calls into this manager? Almost entirely built on callbacks and delegates too.
So naturally, I reached for async/await. Because it's 2026, right? Why would you build anything with callbacks anymore?
I got halfway through the manager before something started to feel off. I had AsyncStreams feeding into completion handlers feeding back into delegate methods. I had bridges on top of bridges. The "modern" code I was writing was making the rest of the system harder to read, not easier.
And then it clicked: I had built a beautiful Tesla engine and was now trying to bolt it onto a horse carriage. 😂
The Trap of "Modern by Default"
Here's what kept happening in my head:
"Concurrency is the future. Combine is being deprecated. Delegates are old. Use the new stuff."
That's not a wrong instinct. But it's also not a complete one. Because the moment you write a single line of code, you're not writing it in a vacuum — you're writing it inside a system.
And the system has a personality. It has habits. It has patterns it already speaks fluently.
When I picked async/await for that manager, I wasn't asking: does this fit? I was asking: is this modern?
Those are two completely different questions.
Let me paint the real picture. Here's what the calling code looked like:
// Every call site in the codebase looks like this:
someService.fetchSomething { [weak self] result in
switch result {
case .success(let data):
self?.delegate?.didFetch(data)
case .failure(let error):
self?.delegate?.didFail(with: error)
}
}
Callbacks. Delegates. A [weak self] dance everyone in the codebase already knows by heart.
And here's what I was about to ship:
// My "modern" manager:
actor MyShinyManager {
func doTheThing() async throws -> Result {
// beautiful, isolated, type-safe, modern
}
}
// The call site, suddenly:
someService.fetchSomething { [weak self] result in
Task { [weak self] in
do {
let value = try await self?.manager.doTheThing()
self?.delegate?.didFetch(value)
} catch {
self?.delegate?.didFail(with: error)
}
}
}
Look at that. Look at it. I added a Task inside a callback inside a delegate. I introduced a new mental model — actors, isolation, structured concurrency — into a file whose entire job is to be a callback.
The manager was clean. The call site was a war crime. The manger didn't fit the system.
The Realization
The lightbulb moment hit me like a truck when I relized that the manager wasn't being used in isolation. It was a participant in a system.
And a participant that doesn't speak the local language forces everyone around it to translate.
Every team member who touches that call site now has to context-switch:
- "Wait, why is there a
Taskhere?" - "Is this
awaitcancellable from the delegate's perspective?" - "If the delegate gets deallocated mid-await, what happens?"
- "Why does this one file look different from the other 200 files in the project?"
I wasn't reducing complexity. I was relocating it — from the manager into every single call site.
Wrong. So wrong.
The Right Question
Here's the question I should have asked from the start:
What's the simplest thing that fits the system I'm working in?
Not "what's newest." Not "what would look impressive in a tech talk." Not "what would I build if this were a greenfield project."
What fits.
If 95% of my codebase speaks delegates and callbacks, then my manager should probably speak delegates and callbacks too — at least at its boundary.
Maybe I use async/await internally for clarity. Maybe I expose both APIs. But the interface needs to match the world it lives in.
// The version I should have shipped:
final class MyManager {
weak var delegate: MyManagerDelegate?
func doTheThing() {
Task {
// I can still use modern concurrency INSIDE
// — the outside world doesn't need to know
do {
let value = try await internalAsyncWork()
await MainActor.run {
delegate?.manager(self, didFinishWith: value)
}
} catch {
await MainActor.run {
delegate?.manager(self, didFailWith: error)
}
}
}
}
}
Same modern internals. Familiar external shape. Zero translation tax at the call sites.
The Broader Lesson
This isn't just about concurrency. It's about every architectural choice we make.
- Your team uses MVC and you read about TCA on a Tuesday? Don't refactor on Wednesday.
- Your codebase is UIKit and SwiftUI just dropped a cool API? Cool. Doesn't mean every screen flips overnight.
- Your project uses Combine and
AsyncSequencelooks tempting? Make sure the boundary still feels native.
Modern isn't a value. Fit is a value.
A pattern is only "good" relative to the system it lives in. A perfect actor-based manager in a callback-heavy codebase is worse than a competent delegate-based manager — because the actor-based one taxes everyone who uses it, forever.
When You Should Reach for the New Thing
I'm not saying never modernize. I'm saying: modernize with intent, not by default.
Reach for the new pattern when:
- → You're starting a new module with no existing callers
- → The team has agreed to migrate and there's a plan
- → The new pattern genuinely removes complexity, not just relocates it
- → You can introduce it at a boundary that hides the seam
Skip the new pattern when:
- → Every call site would need a
Task { }wrapper or a Combine bridge - → You're the only one on the team who's comfortable with it
- → "It's more modern" is the strongest argument you have
Conclusion
I rewrote the manager. The internals still use structured concurrency where it makes sense — but the API it exposes looks and feels like every other manager in the codebase. The call sites went back to being boring.
And boring, in this context, is a feature.
The next time you reach for the shiniest tool in your toolbox, take a moment to look at the house you're building it into.
A diamond-tipped drill is incredible. But if every other tool on the job site is a hammer, sometimes the right move is to pick up the hammer.
You're not writing code in isolation — you're contributing to a system. And the best code isn't the most modern. It's the code that the next person on your team reads without flinching.
subscribe for more like this