<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en_US"><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://yousufsohail.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://yousufsohail.com/" rel="alternate" type="text/html" hreflang="en_US" /><updated>2026-03-27T04:32:29+00:00</updated><id>https://yousufsohail.com/feed.xml</id><title type="html">Yousuf Sohail</title><subtitle>Engineering Manager and product engineer with 13+ years building mobile apps at scale. Writing about Android, Kotlin, KMM, team leadership, and things that matter.</subtitle><author><name>Yousuf Sohail</name></author><entry><title type="html">My first 30 days as an engineering manager</title><link href="https://yousufsohail.com/writing/ic-to-em-first-30-days/" rel="alternate" type="text/html" title="My first 30 days as an engineering manager" /><published>2026-03-10T00:00:00+00:00</published><updated>2026-03-10T00:00:00+00:00</updated><id>https://yousufsohail.com/writing/ic-to-em-first-30-days</id><content type="html" xml:base="https://yousufsohail.com/writing/ic-to-em-first-30-days/"><![CDATA[<p>I became an Engineering Manager on February 1, 2026. Thirty days later, here’s what actually changed — and what I didn’t expect.</p>

<h2 id="what-i-expected-to-change">What I Expected to Change</h2>

<p><strong>More meetings.</strong> True. Where I had two-hour focus blocks every morning, I now have 1:1s, sprint planning, stakeholder syncs, and cross-squad conversations. I protect afternoons for deeper work.</p>

<p><strong>Less coding.</strong> True, mostly. I still review PRs and do architecture deep dives when I need to understand a problem before delegating it. But the line-by-line, feature-shipping coding is done. I miss it sometimes. Not enough to think I’ve made the wrong choice.</p>

<p><strong>More ambiguity.</strong> True, and harder to prepare for. As an IC, “done” was legible — the test passed, the feature shipped, the PR merged. As an EM, “done” is harder to see. Is the team healthy? Is the architecture pointed in the right direction? Are the right people growing? These are slow variables with long feedback loops.</p>

<h2 id="what-i-didnt-expect">What I Didn’t Expect</h2>

<p><strong>The context-switching tax is different.</strong></p>

<p>As an engineer, context-switching meant switching between tasks — feature to PR review to bug fix. Expensive but recoverable.</p>

<p>As an EM, it means switching between people and between problems that live entirely in different heads. One minute I’m helping an engineer think through a data model. Next I’m in a conversation with a product manager about scope. Next I’m in a 1:1 about someone’s growth trajectory.</p>

<p>Each requires a different mode of thinking. The cost isn’t in the transition — it’s in arriving at each conversation fully present. Some days I’m good at this. Some days I’m not.</p>

<p><strong>The leverage is real but invisible.</strong></p>

<p>The most important thing I’ve done in my first 30 days probably doesn’t appear in any metric yet.</p>

<p>I had a conversation with one of my engineers about what they wanted their career to look like in three years. They hadn’t been asked that question directly before. By the end, they had a direction. Their work since then has had a different quality — more intentional, more engaged.</p>

<p>Did that conversation show up in sprint velocity? No. Will it compound into better work over the next year? Probably.</p>

<p>That’s the EM leverage model. Invisible inputs, slow outputs. You have to trust the model before you can measure the results.</p>

<p><strong>People problems are architecture problems.</strong></p>

<p>I knew engineering teams had “people problems.” I thought they were separate from “technical problems.” They’re not.</p>

<p>In my first 30 days:</p>
<ul>
  <li>Two engineers communicate in ways that create unclear module ownership. Result: duplicated code, inconsistent patterns. This is an architecture problem and a communication problem simultaneously.</li>
  <li>No one is sure who makes the final call on API contract decisions. Result: delays on every integration. This is a process problem and an architecture problem about where decisions live.</li>
</ul>

<p>The codebase is a reflection of the communication patterns of the team that built it — Conway’s Law, which every engineer has heard and most have underestimated. My job now is to shape both.</p>

<h2 id="what-im-still-figuring-out">What I’m Still Figuring Out</h2>

<p><strong>When to direct vs. when to coach.</strong></p>

<p>I have opinions. Thirteen years of engineering experience give you opinions. The reflex when someone presents an approach I disagree with is to say: “I’d do it this way.”</p>

<p>That’s not always right. Sometimes the engineer’s approach is fine — different from mine but fine — and what they need is confidence to ship, not a redirect. Sometimes the approach has a real problem that needs to surface through questions, not instructions.</p>

<p>I’m still calibrating this. I’ll be calibrating it for years.</p>

<p><strong>How to measure team health before it surfaces in metrics.</strong></p>

<p>Sprint velocity is a lagging indicator. By the time velocity drops, something has been wrong for weeks. I’m looking for leading indicators — the texture of standups, whether engineers ask for help or stay stuck, whether PRs move smoothly or accumulate review comments.</p>

<p>I don’t have a good system for this yet. It’s on the list.</p>

<h2 id="the-honest-take-after-30-days">The Honest Take After 30 Days</h2>

<p>The best thing: the potential impact. Helping five engineers do better work over the next year is higher leverage than the code I would have written as an IC.</p>

<p>The hardest thing: the patience required. As an IC, you build something in a sprint and can point to it. As an EM, you build something in a quarter that you can only point to obliquely.</p>

<p>I chose this. I’d choose it again.</p>

<p>Ask me in a year.</p>]]></content><author><name>Yousuf Sohail</name></author><category term="career" /><category term="engineering" /><category term="management" /><summary type="html"><![CDATA[I became an Engineering Manager on February 1, 2026. Thirty days later, here’s what actually changed — and what I didn’t expect.]]></summary></entry><entry><title type="html">Six months of KMM in production</title><link href="https://yousufsohail.com/writing/kmm-in-production/" rel="alternate" type="text/html" title="Six months of KMM in production" /><published>2026-01-15T00:00:00+00:00</published><updated>2026-01-15T00:00:00+00:00</updated><id>https://yousufsohail.com/writing/kmm-in-production</id><content type="html" xml:base="https://yousufsohail.com/writing/kmm-in-production/"><![CDATA[<p>I evaluated KMM at Delivery Hero in late 2022. Verdict: promising direction, not yet ready for full adoption. <a href="/writing/kmm-evaluation/">Read that post here.</a></p>

<p>At Cashia, I joined a team already running KMM in production. Six months in, here’s the updated picture.</p>

<h2 id="what-changed-between-2022-and-now">What Changed Between 2022 and Now</h2>

<p>A lot.</p>

<p><code class="language-plaintext highlighter-rouge">SKIE</code> (Swift/Kotlin Interface Enhancer) has transformed the iOS integration story. In 2022, getting Kotlin flows and coroutines to work ergonomically from Swift required manual wrappers and ceremony. SKIE generates idiomatic Swift interfaces automatically. Our iOS engineers use shared KMM code in ways that feel native — not like wrapping a foreign library.</p>

<p>The plugin ecosystem has matured. <code class="language-plaintext highlighter-rouge">kotlinx.serialization</code>, <code class="language-plaintext highlighter-rouge">Ktor</code>, <code class="language-plaintext highlighter-rouge">SQLDelight</code> all have solid KMM support and we use all three in production.</p>

<p>Debugging has improved. Breakpoints in shared KMM code work from both Android Studio and Xcode for common cases.</p>

<h2 id="the-architecture-at-cashia">The Architecture at Cashia</h2>

<p>The shared module owns:</p>
<ul>
  <li>Domain models and business logic</li>
  <li>API clients and serialization (Ktor + kotlinx.serialization)</li>
  <li>Local persistence (SQLDelight)</li>
  <li>Repository interfaces and implementations</li>
  <li>UseCases</li>
</ul>

<p>Platform-specific code owns:</p>
<ul>
  <li>ViewModels (Android) and ObservableObjects (iOS)</li>
  <li>UI (Compose on Android, SwiftUI on iOS)</li>
  <li>Platform API wrappers (biometrics, notifications, deep links)</li>
</ul>

<p>This boundary has held well. Friction lives at the edges — anything crossing from shared to platform code — but the core is clean.</p>

<h2 id="whats-actually-shared-vs-what-isnt">What’s Actually Shared vs What Isn’t</h2>

<p><strong>Shared (works well):</strong></p>
<ul>
  <li>API models and serialization</li>
  <li>Network error handling and retry logic</li>
  <li>Business logic — validations, calculations, state machines</li>
  <li>Repository implementations</li>
  <li>Flow-based data streams</li>
</ul>

<p><strong>Platform-specific:</strong></p>
<ul>
  <li>ViewModels / ObservableObjects — lifecycle semantics differ too much</li>
  <li>File system operations — path handling is platform-specific</li>
  <li>Cryptography — we use platform keystores (Android Keystore, iOS Secure Enclave)</li>
  <li>Push notification handling</li>
  <li>Biometric authentication</li>
</ul>

<p><strong>The grey zone:</strong></p>
<ul>
  <li>SQLDelight — driver is platform-specific, queries and schema are shared. Solution: define schema in shared, provide platform driver via DI. Works well.</li>
  <li>Image loading — no good cross-platform story. Coil on Android, AsyncImage on iOS. Accepted divergence.</li>
</ul>

<h2 id="the-viewmodel-boundary">The ViewModel Boundary</h2>

<p>The most common KMM architecture mistake: sharing ViewModels.</p>

<p>Tempting — you’ve already shared repositories and use cases. Why not go further?</p>

<p>Because ViewModels on Android are lifecycle-aware. On iOS, the equivalent is <code class="language-plaintext highlighter-rouge">ObservableObject</code> with SwiftUI’s <code class="language-plaintext highlighter-rouge">@StateObject</code>. Lifecycle semantics differ. State management primitives differ. Observable state differs.</p>

<p>Sharing ViewModels means writing the lowest-common-denominator — losing platform-specific behaviour — or adding platform abstractions that make the shared ViewModel as complex as just writing two separate ones.</p>

<p>Write separate ViewModels. Have them call the same use cases. This is the right boundary.</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// shared module</span>
<span class="kd">class</span> <span class="nc">GetTransactionHistoryUseCase</span><span class="p">(</span>
    <span class="k">private</span> <span class="kd">val</span> <span class="py">repository</span><span class="p">:</span> <span class="nc">TransactionRepository</span>
<span class="p">)</span> <span class="p">{</span>
    <span class="k">operator</span> <span class="k">fun</span> <span class="nf">invoke</span><span class="p">():</span> <span class="nc">Flow</span><span class="p">&lt;</span><span class="nc">List</span><span class="p">&lt;</span><span class="nc">Transaction</span><span class="p">&gt;&gt;</span> <span class="p">=</span> <span class="n">repository</span><span class="p">.</span><span class="nf">getTransactions</span><span class="p">()</span>
<span class="p">}</span>

<span class="c1">// androidMain</span>
<span class="kd">class</span> <span class="nc">TransactionViewModel</span><span class="p">(</span>
    <span class="k">private</span> <span class="kd">val</span> <span class="py">getHistory</span><span class="p">:</span> <span class="nc">GetTransactionHistoryUseCase</span>
<span class="p">)</span> <span class="p">:</span> <span class="nc">ViewModel</span><span class="p">()</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">transactions</span> <span class="p">=</span> <span class="nf">getHistory</span><span class="p">()</span>
        <span class="p">.</span><span class="nf">stateIn</span><span class="p">(</span><span class="n">viewModelScope</span><span class="p">,</span> <span class="nc">SharingStarted</span><span class="p">.</span><span class="nc">WhileSubscribed</span><span class="p">(</span><span class="mi">5000</span><span class="p">),</span> <span class="nf">emptyList</span><span class="p">())</span>
<span class="p">}</span>
</code></pre></div></div>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// iOS — Swift</span>
<span class="kd">class</span> <span class="kt">TransactionViewModel</span><span class="p">:</span> <span class="kt">ObservableObject</span> <span class="p">{</span>
    <span class="kd">@Published</span> <span class="k">var</span> <span class="nv">transactions</span><span class="p">:</span> <span class="p">[</span><span class="kt">Transaction</span><span class="p">]</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="kd">private</span> <span class="k">let</span> <span class="nv">getHistory</span><span class="p">:</span> <span class="kt">GetTransactionHistoryUseCase</span>

    <span class="nf">init</span><span class="p">(</span><span class="nv">getHistory</span><span class="p">:</span> <span class="kt">GetTransactionHistoryUseCase</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">self</span><span class="o">.</span><span class="n">getHistory</span> <span class="o">=</span> <span class="n">getHistory</span>
        <span class="nf">getHistory</span><span class="p">()</span><span class="o">.</span><span class="n">watch</span> <span class="p">{</span> <span class="p">[</span><span class="k">weak</span> <span class="k">self</span><span class="p">]</span> <span class="n">result</span> <span class="k">in</span>
            <span class="k">self</span><span class="p">?</span><span class="o">.</span><span class="n">transactions</span> <span class="o">=</span> <span class="n">result</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="the-architecture-migration">The Architecture Migration</h2>

<p>Cashia started with a layer-based structure: <code class="language-plaintext highlighter-rouge">data</code>, <code class="language-plaintext highlighter-rouge">domain</code>, <code class="language-plaintext highlighter-rouge">presentation</code> as top-level layers with features spread across them. The problem at scale: adding a feature means touching all three layers, and boundaries blur as the team grows.</p>

<p>We’re migrating to feature-module structure: each feature owns its data, domain, and presentation internally. In KMM, each feature module exposes a clean public API, hiding internals from the Android and iOS apps.</p>

<p>Ongoing. One feature at a time, never mid-sprint. The pattern is right.</p>

<h2 id="should-you-use-kmm">Should You Use KMM?</h2>

<p>2022: “promising, not yet.”</p>

<p>2026: “yes, for the right cases.”</p>

<p><strong>Right cases:</strong> You have Android and iOS teams with genuine parity problems. Your business logic is complex enough that maintaining it twice is costly. Your iOS team knows Swift. You’re willing to invest in the shared module architecture upfront.</p>

<p><strong>Wrong cases:</strong> Very small team — just pick one platform. UI-heavy app with minimal business logic — nothing to share. Team without strong Kotlin expertise — the shared module is always Kotlin.</p>

<p>KMM is ready. The question is whether your problem warrants it.</p>]]></content><author><name>Yousuf Sohail</name></author><category term="android" /><category term="development" /><category term="kotlin" /><category term="kmm" /><summary type="html"><![CDATA[I evaluated KMM at Delivery Hero in late 2022. Verdict: promising direction, not yet ready for full adoption. Read that post here.]]></summary></entry><entry><title type="html">Nairobi changed how I think about the code I write</title><link href="https://yousufsohail.com/writing/nairobi-changed-how-i-think/" rel="alternate" type="text/html" title="Nairobi changed how I think about the code I write" /><published>2025-12-10T00:00:00+00:00</published><updated>2025-12-10T00:00:00+00:00</updated><id>https://yousufsohail.com/writing/nairobi-changed-how-i-think</id><content type="html" xml:base="https://yousufsohail.com/writing/nairobi-changed-how-i-think/"><![CDATA[<p>I booked the flight to Nairobi six weeks into my role at Cashia. Not because I was asked. Because I wanted to understand where the product lived.</p>

<p>I’d been writing code for five months against a Kenyan market — APIs for M-PESA transactions, payment flows built around Kenyan mobile money patterns, a consumer app for users who interact with money differently than I do. But I’d been doing it from Dubai, in a coworking space, on a MacBook.</p>

<p>The code worked. But I was building abstractions of a place I’d never been.</p>

<p>So I went.</p>

<h2 id="what-i-expected">What I Expected</h2>

<p>I expected Nairobi to be “a city.” I’d been to many cities. I thought I knew what that meant.</p>

<p>The matatus — minibuses with bold graphics and bolder horns — navigate the roads with a logic you understand after a few days but can’t describe in rules. The city moves with an energy that doesn’t match the “developing market” framing I’d encountered in product meetings.</p>

<p>The engineers I worked with had the same arguments engineers have everywhere. Which state management pattern is better. Whether to abstract early or wait for duplication. How much technical debt is acceptable in a pre-launch sprint.</p>

<p>The same arguments. The same craft. Different context.</p>

<h2 id="the-m-pesa-moment">The M-PESA Moment</h2>

<p>I sat with one of the Kenyan engineers and we walked through the M-PESA cash-out flow I’d built from Dubai.</p>

<p>He pointed out something I’d never have caught remotely.</p>

<p>In Kenya, M-PESA transactions have a cultural rhythm. Users check their M-PESA balance frequently — more than a bank balance — because M-PESA is the primary account for many people, not a secondary wallet. The transaction status screen I’d built was technically correct but behaviourally wrong: it showed “processing” without prominently surfacing the updated balance. In a market where the balance confirmation is the meaningful signal, not the processing indicator, the screen was optimised for the wrong thing.</p>

<p>Fifteen minutes of conversation. The insight came from being in the same room.</p>

<p>I went back and redesigned the confirmation screen that week.</p>

<h2 id="nairobi-national-park">Nairobi National Park</h2>

<p>On the weekend, I went to Nairobi National Park.</p>

<p>There are giraffes in Nairobi National Park. And beyond them, visible through the trees, is the Nairobi city skyline.</p>

<p>I took a photo I’ve thought about more than any other photo I’ve taken. The giraffe. The city. The same frame.</p>

<p>What I remember more than the photo is what I was thinking when I took it.</p>

<p>I was thinking: the people in those buildings are the users. Not “users” as an abstraction in a product spec. Actual people, in actual buildings, using an app I’m building to do actual transactions that matter to their daily lives.</p>

<p>The abstraction collapsed.</p>

<p>I flew back to Dubai. The code didn’t change. My relationship to it did.</p>

<h2 id="why-im-writing-this">Why I’m Writing This</h2>

<p>Software engineers spend a lot of time optimising for things they can measure. Performance benchmarks. Test coverage. Delivery velocity.</p>

<p>Those things matter. But there’s a category of understanding that doesn’t show up in metrics — the contextual understanding of why the product you’re building matters to the people it serves.</p>

<p>That understanding changes what you build. It changes what you think is worth arguing about. It changes how you feel when a feature ships.</p>

<p>If you’re building a product for a market you’ve never been to, and you have any opportunity to go there: go.</p>

<p>It costs a week. It pays back for years.</p>

<h2 id="what-changed-concretely">What Changed Concretely</h2>

<p>After Nairobi:</p>
<ul>
  <li>I started asking “what does this look like on 4G with intermittent drops” before “what does this look like on my MacBook on WiFi”</li>
  <li>I started treating transaction feedback UI as first-class, not nice-to-have</li>
  <li>I started thinking about who the user is before I think about what the feature is</li>
</ul>

<p>Small shifts. They compound.</p>

<p>Seven months after that trip, I became Engineering Manager for the squad that owns the experience those users have every day.</p>

<p>I don’t think that’s a coincidence.</p>]]></content><author><name>Yousuf Sohail</name></author><category term="life" /><category term="career" /><summary type="html"><![CDATA[I booked the flight to Nairobi six weeks into my role at Cashia. Not because I was asked. Because I wanted to understand where the product lived.]]></summary></entry><entry><title type="html">Kotlin is my answer. Here’s why it’s still not everyone’s.</title><link href="https://yousufsohail.com/writing/why-kotlin/" rel="alternate" type="text/html" title="Kotlin is my answer. Here’s why it’s still not everyone’s." /><published>2025-12-01T00:00:00+00:00</published><updated>2025-12-01T00:00:00+00:00</updated><id>https://yousufsohail.com/writing/why-kotlin</id><content type="html" xml:base="https://yousufsohail.com/writing/why-kotlin/"><![CDATA[<p>I’ve been writing Kotlin since 2017. Before it was the default. Before Google recommended it. Before every Android tutorial assumed it.</p>

<p>Eight years later, it’s still the language I reach for first. But not because it won.</p>

<h2 id="the-argument-everyone-makes">The argument everyone makes</h2>

<p>“Kotlin is more concise.” “Kotlin has null safety.” “Kotlin has coroutines.”</p>

<p>All true. All boring reasons.</p>

<p>Conciseness is nice. Null safety catches bugs. Coroutines are powerful. But if these were the only reasons, Kotlin would be a minor upgrade — Java with better syntax.</p>

<p>The real reason I use Kotlin is different.</p>

<h2 id="kotlin-fits-how-i-think">Kotlin fits how I think</h2>

<p>When I model a domain — a payment flow, a feature flag, a user session — Kotlin lets me express the constraints in the type system.</p>

<p>A sealed class for payment states means the compiler tells me when I’ve missed a case. A data class for a transaction means equality works without boilerplate. An extension function on a domain type means the behavior lives next to the thing it describes.</p>

<p>These aren’t features. They’re design tools. They let me encode intent in a way that Java never did.</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">sealed</span> <span class="kd">interface</span> <span class="nc">PaymentState</span> <span class="p">{</span>
    <span class="n">data</span> <span class="kd">object</span> <span class="nc">Idle</span> <span class="p">:</span> <span class="nc">PaymentState</span>
    <span class="kd">data class</span> <span class="nc">Processing</span><span class="p">(</span><span class="kd">val</span> <span class="py">transactionId</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span> <span class="p">:</span> <span class="nc">PaymentState</span>
    <span class="kd">data class</span> <span class="nc">Completed</span><span class="p">(</span><span class="kd">val</span> <span class="py">receipt</span><span class="p">:</span> <span class="nc">Receipt</span><span class="p">)</span> <span class="p">:</span> <span class="nc">PaymentState</span>
    <span class="kd">data class</span> <span class="nc">Failed</span><span class="p">(</span><span class="kd">val</span> <span class="py">reason</span><span class="p">:</span> <span class="nc">FailureReason</span><span class="p">)</span> <span class="p">:</span> <span class="nc">PaymentState</span>
<span class="p">}</span>

<span class="c1">// The compiler enforces exhaustive handling.</span>
<span class="c1">// Miss a state and the build fails.</span>
<span class="c1">// That's not convenience. That's correctness.</span>
</code></pre></div></div>

<h2 id="the-kmm-factor">The KMM factor</h2>

<p>At Cashia, we run Kotlin Multiplatform. One codebase for business logic, shared across Android and iOS. I wrote about this in detail <a href="/writing/kmm-in-production/">here</a>.</p>

<p>KMM is the strongest argument for Kotlin that has nothing to do with syntax. If you write your domain layer in Kotlin, you can share it. That’s not a language feature — it’s a platform play.</p>

<h2 id="what-about-flutter-what-about-swift">What about Flutter? What about Swift?</h2>

<p>Flutter is excellent for certain products. Cross-platform UI with a single codebase. If I were building a content app or a utility, I’d consider it seriously.</p>

<p>But for fintech — where platform-native behavior matters, where you need deep access to payment SDKs, where the user’s trust depends on the app feeling native — Flutter adds friction I don’t want.</p>

<p>Swift is a great language. If I were iOS-only, I’d use it. But I’m not iOS-only, and KMM means I don’t have to choose between platforms and code sharing.</p>

<h2 id="the-honest-downsides">The honest downsides</h2>

<p>Kotlin’s build times are worse than Java’s. The tooling, while improving, still has rough edges — especially for multiplatform. And the learning curve for coroutines is steeper than most tutorials admit.</p>

<p>I also see teams adopt Kotlin and write Java with Kotlin syntax. That’s worse than writing Java. Kotlin rewards idiomatic usage. If you’re not going to learn the idioms, don’t switch.</p>

<h2 id="why-its-still-not-universal">Why it’s still not universal</h2>

<p>Kotlin won Android. But it didn’t win everything.</p>

<p>Backend teams that are happy with Java 21 don’t need Kotlin. iOS teams have Swift. Web teams have TypeScript. Each language has a context where it’s the natural choice.</p>

<p>Kotlin is mine because my context — mobile, multiplatform, fintech, type safety as a requirement — aligns perfectly with what it does well.</p>

<p>That’s not a universal truth. It’s a local one.</p>

<hr />

<p><em>The best language is the one that fits how you think and what you’re building. For me, that’s been Kotlin for eight years. Ask me again in eight more.</em></p>]]></content><author><name>Yousuf Sohail</name></author><category term="development" /><category term="android" /><summary type="html"><![CDATA[I’ve been writing Kotlin since 2017. Before it was the default. Before Google recommended it. Before every Android tutorial assumed it.]]></summary></entry><entry><title type="html">Force-update your app without making users hate you</title><link href="https://yousufsohail.com/writing/forced-app-updates/" rel="alternate" type="text/html" title="Force-update your app without making users hate you" /><published>2025-10-20T00:00:00+00:00</published><updated>2025-10-20T00:00:00+00:00</updated><id>https://yousufsohail.com/writing/forced-app-updates</id><content type="html" xml:base="https://yousufsohail.com/writing/forced-app-updates/"><![CDATA[<p>Forced app updates are one of those features nobody asks for, nobody notices when they work, and everybody notices when they’re missing.</p>

<p>I built a forced update mechanism at Cashia before our first public launch. It’s been running in production since day one.</p>

<h2 id="the-problem-without-forced-updates">The Problem Without Forced Updates</h2>

<p>You ship v1.0. Three months later, you ship v1.3 with a critical security fix. You push it to the Play Store.</p>

<p>Now what?</p>

<p>Some users update immediately. Some eventually. Some have auto-update disabled and won’t update for months. Six months on, you still have users on v1.0.</p>

<p>In a social app, this is manageable. In a payments app, users on a version with an unpatched security vulnerability is a liability. In a regulated fintech, it may be a compliance problem.</p>

<p>More practically: every API version you ship must stay compatible with every app version still in the wild. If 5% of users are on v1.0, your backend cannot remove any endpoint that v1.0 uses. The tail of old versions compounds into technical debt.</p>

<p>Forced updates give you a clean mechanism to say: below this version, we require an update to proceed.</p>

<h2 id="the-two-types">The Two Types</h2>

<p><strong>Flexible update</strong>: “A new version is available. Update when convenient.” User can dismiss. Good for feature releases.</p>

<p><strong>Forced update</strong>: “You must update before you can use the app.” User cannot dismiss. For security fixes, critical bugs, API deprecations.</p>

<h2 id="the-server-side-approach">The Server-Side Approach</h2>

<p>Google Play’s in-app update API works well but requires Google Play Services. For Cashia — where we needed a solution that worked across distribution channels — we implemented server-side version checking.</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">data class</span> <span class="nc">VersionCheckResponse</span><span class="p">(</span>
    <span class="kd">val</span> <span class="py">minimumVersion</span><span class="p">:</span> <span class="nc">String</span><span class="p">,</span>
    <span class="kd">val</span> <span class="py">recommendedVersion</span><span class="p">:</span> <span class="nc">String</span><span class="p">,</span>
    <span class="kd">val</span> <span class="py">forceUpdateMessage</span><span class="p">:</span> <span class="nc">String</span><span class="p">?,</span>
    <span class="kd">val</span> <span class="py">storeUrl</span><span class="p">:</span> <span class="nc">String</span>
<span class="p">)</span>

<span class="kd">class</span> <span class="nc">VersionCheckUseCase</span><span class="p">(</span>
    <span class="k">private</span> <span class="kd">val</span> <span class="py">api</span><span class="p">:</span> <span class="nc">VersionApi</span><span class="p">,</span>
    <span class="k">private</span> <span class="kd">val</span> <span class="py">currentVersion</span><span class="p">:</span> <span class="nc">String</span>
<span class="p">)</span> <span class="p">{</span>
    <span class="k">suspend</span> <span class="k">fun</span> <span class="nf">check</span><span class="p">():</span> <span class="nc">VersionCheckResult</span> <span class="p">{</span>
        <span class="kd">val</span> <span class="py">response</span> <span class="p">=</span> <span class="n">api</span><span class="p">.</span><span class="nf">checkVersion</span><span class="p">(</span><span class="n">currentVersion</span><span class="p">)</span>
        <span class="k">return</span> <span class="k">when</span> <span class="p">{</span>
            <span class="n">currentVersion</span><span class="p">.</span><span class="nf">isOlderThan</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">minimumVersion</span><span class="p">)</span> <span class="p">-&gt;</span>
                <span class="nc">VersionCheckResult</span><span class="p">.</span><span class="nc">ForceUpdate</span><span class="p">(</span>
                    <span class="n">message</span> <span class="p">=</span> <span class="n">response</span><span class="p">.</span><span class="n">forceUpdateMessage</span> <span class="o">?:</span> <span class="s">"Please update to continue."</span><span class="p">,</span>
                    <span class="n">storeUrl</span> <span class="p">=</span> <span class="n">response</span><span class="p">.</span><span class="n">storeUrl</span>
                <span class="p">)</span>
            <span class="n">currentVersion</span><span class="p">.</span><span class="nf">isOlderThan</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">recommendedVersion</span><span class="p">)</span> <span class="p">-&gt;</span>
                <span class="nc">VersionCheckResult</span><span class="p">.</span><span class="nc">OptionalUpdate</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">storeUrl</span><span class="p">)</span>
            <span class="k">else</span> <span class="p">-&gt;</span>
                <span class="nc">VersionCheckResult</span><span class="p">.</span><span class="nc">UpToDate</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="k">sealed</span> <span class="kd">class</span> <span class="nc">VersionCheckResult</span> <span class="p">{</span>
    <span class="kd">data class</span> <span class="nc">ForceUpdate</span><span class="p">(</span><span class="kd">val</span> <span class="py">message</span><span class="p">:</span> <span class="nc">String</span><span class="p">,</span> <span class="kd">val</span> <span class="py">storeUrl</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span> <span class="p">:</span> <span class="nc">VersionCheckResult</span><span class="p">()</span>
    <span class="kd">data class</span> <span class="nc">OptionalUpdate</span><span class="p">(</span><span class="kd">val</span> <span class="py">storeUrl</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span> <span class="p">:</span> <span class="nc">VersionCheckResult</span><span class="p">()</span>
    <span class="kd">object</span> <span class="nc">UpToDate</span> <span class="p">:</span> <span class="nc">VersionCheckResult</span><span class="p">()</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The version check fires on every app foreground. A <code class="language-plaintext highlighter-rouge">ForceUpdate</code> result shows a non-dismissable dialog with a single “Update Now” action.</p>

<h2 id="what-people-get-wrong">What People Get Wrong</h2>

<p><strong>Blocking on network.</strong> Don’t make the app unusable while the version check is pending. Run it in the background on startup. Show the dialog only when you have a confirmed response. A slow network should not block the user from opening the app.</p>

<p><strong>Not communicating why.</strong> “Please update to continue” is bad UX. “We’ve improved the security of your payments — please update to continue” is better. Users are more likely to update when they understand the reason.</p>

<p><strong>Using forced updates too often.</strong> If you force-update for every release, users learn to resent it. Reserve forced updates for security fixes and API-breaking changes. Use recommended updates for everything else.</p>

<h2 id="the-launch-day-payoff">The Launch Day Payoff</h2>

<p>We shipped the forced update mechanism in v1.0 — the very first public release.</p>

<p>Three months later, we had our first critical bug. The fix was in v1.1. We set the minimum version to v1.1 and pushed the force update configuration.</p>

<p>Within 24 hours, adoption of the fix was essentially complete. No support tickets from users on the broken version. No backend compatibility maintenance.</p>

<p>Build it before you need it. The cost of not having it lands at the worst possible time.</p>]]></content><author><name>Yousuf Sohail</name></author><category term="android" /><category term="development" /><category term="product" /><summary type="html"><![CDATA[Forced app updates are one of those features nobody asks for, nobody notices when they work, and everybody notices when they’re missing.]]></summary></entry><entry><title type="html">The feature flag framework we shipped at Cashia</title><link href="https://yousufsohail.com/writing/feature-flags-android/" rel="alternate" type="text/html" title="The feature flag framework we shipped at Cashia" /><published>2025-09-10T00:00:00+00:00</published><updated>2025-09-10T00:00:00+00:00</updated><id>https://yousufsohail.com/writing/feature-flags-android</id><content type="html" xml:base="https://yousufsohail.com/writing/feature-flags-android/"><![CDATA[<p>Feature flags are one of those things every team says they want and few implement well.</p>

<p>At Cashia, I built a feature flag framework from scratch, presented it at a company-wide tech demo, and drove adoption across all squads. Every critical feature now ships behind a flag.</p>

<h2 id="why-feature-flags-matter">Why Feature Flags Matter</h2>

<p>The obvious use case: hide an incomplete feature during development. But that’s not the main reason to invest in a proper framework.</p>

<p><strong>Gradual rollouts.</strong> Ship to 1% of users. Watch crash rates and error logs. If nothing breaks, roll to 10%, then 50%, then 100%. If something breaks, flip a flag — not a hotfix.</p>

<p><strong>Kill switches.</strong> If a payment flow has a critical bug in production, you need to disable it in seconds without a deployment.</p>

<p><strong>A/B testing.</strong> The mechanism for testing two versions of a UI.</p>

<p>At a pre-launch startup: the kill switch matters most. Launch day is when you discover the things testing didn’t find.</p>

<h2 id="the-design">The Design</h2>

<p>Four properties I wanted:</p>

<ol>
  <li><strong>Simple API</strong> — adding a new flag touches one file</li>
  <li><strong>Testable</strong> — flag-dependent code is unit-testable without mocking remote config</li>
  <li><strong>Safe defaults</strong> — if remote config is unavailable, behave conservatively</li>
  <li><strong>Observable</strong> — flag changes are reactive, UI responds without restart</li>
</ol>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// One file for all flag definitions</span>
<span class="kd">object</span> <span class="nc">FeatureFlags</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">CARD_TOP_UP</span> <span class="p">=</span> <span class="nc">Flag</span><span class="p">(</span><span class="s">"card_top_up"</span><span class="p">,</span> <span class="n">defaultValue</span> <span class="p">=</span> <span class="k">false</span><span class="p">)</span>
    <span class="kd">val</span> <span class="py">NEW_ONBOARDING_FLOW</span> <span class="p">=</span> <span class="nc">Flag</span><span class="p">(</span><span class="s">"new_onboarding_flow"</span><span class="p">,</span> <span class="n">defaultValue</span> <span class="p">=</span> <span class="k">false</span><span class="p">)</span>
    <span class="kd">val</span> <span class="py">MPESA_CASHOUT</span> <span class="p">=</span> <span class="nc">Flag</span><span class="p">(</span><span class="s">"mpesa_cashout"</span><span class="p">,</span> <span class="n">defaultValue</span> <span class="p">=</span> <span class="k">false</span><span class="p">)</span>
    <span class="kd">val</span> <span class="py">UNIFIED_TRANSACTION_HISTORY</span> <span class="p">=</span> <span class="nc">Flag</span><span class="p">(</span><span class="s">"unified_tx_history"</span><span class="p">,</span> <span class="n">defaultValue</span> <span class="p">=</span> <span class="k">true</span><span class="p">)</span>
<span class="p">}</span>

<span class="kd">data class</span> <span class="nc">Flag</span><span class="p">(</span><span class="kd">val</span> <span class="py">key</span><span class="p">:</span> <span class="nc">String</span><span class="p">,</span> <span class="kd">val</span> <span class="py">defaultValue</span><span class="p">:</span> <span class="nc">Boolean</span><span class="p">)</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">FlagManager</code> interface provides runtime values:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">interface</span> <span class="nc">FlagManager</span> <span class="p">{</span>
    <span class="k">fun</span> <span class="nf">isEnabled</span><span class="p">(</span><span class="n">flag</span><span class="p">:</span> <span class="nc">Flag</span><span class="p">):</span> <span class="nc">Boolean</span>
    <span class="k">fun</span> <span class="nf">observe</span><span class="p">(</span><span class="n">flag</span><span class="p">:</span> <span class="nc">Flag</span><span class="p">):</span> <span class="nc">Flow</span><span class="p">&lt;</span><span class="nc">Boolean</span><span class="p">&gt;</span>
<span class="p">}</span>

<span class="kd">class</span> <span class="nc">RemoteFlagManager</span><span class="p">(</span>
    <span class="k">private</span> <span class="kd">val</span> <span class="py">remoteConfig</span><span class="p">:</span> <span class="nc">FirebaseRemoteConfig</span>
<span class="p">)</span> <span class="p">:</span> <span class="nc">FlagManager</span> <span class="p">{</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="nf">isEnabled</span><span class="p">(</span><span class="n">flag</span><span class="p">:</span> <span class="nc">Flag</span><span class="p">):</span> <span class="nc">Boolean</span> <span class="p">=</span> <span class="k">try</span> <span class="p">{</span>
        <span class="n">remoteConfig</span><span class="p">.</span><span class="nf">getBoolean</span><span class="p">(</span><span class="n">flag</span><span class="p">.</span><span class="n">key</span><span class="p">)</span>
    <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="n">e</span><span class="p">:</span> <span class="nc">Exception</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">flag</span><span class="p">.</span><span class="n">defaultValue</span> <span class="c1">// Safe fallback</span>
    <span class="p">}</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="nf">observe</span><span class="p">(</span><span class="n">flag</span><span class="p">:</span> <span class="nc">Flag</span><span class="p">):</span> <span class="nc">Flow</span><span class="p">&lt;</span><span class="nc">Boolean</span><span class="p">&gt;</span> <span class="p">=</span> <span class="nf">callbackFlow</span> <span class="p">{</span>
        <span class="kd">val</span> <span class="py">listener</span> <span class="p">=</span> <span class="n">remoteConfig</span><span class="p">.</span><span class="nf">addOnConfigUpdateListener</span> <span class="p">{</span> <span class="n">update</span><span class="p">,</span> <span class="n">error</span> <span class="p">-&gt;</span>
            <span class="k">if</span> <span class="p">(</span><span class="n">error</span> <span class="p">==</span> <span class="k">null</span> <span class="p">&amp;&amp;</span> <span class="n">update</span><span class="p">.</span><span class="n">updatedKeys</span><span class="p">.</span><span class="nf">contains</span><span class="p">(</span><span class="n">flag</span><span class="p">.</span><span class="n">key</span><span class="p">))</span> <span class="p">{</span>
                <span class="nf">trySend</span><span class="p">(</span><span class="nf">isEnabled</span><span class="p">(</span><span class="n">flag</span><span class="p">))</span>
            <span class="p">}</span>
        <span class="p">}</span>
        <span class="nf">awaitClose</span> <span class="p">{</span> <span class="n">listener</span><span class="p">.</span><span class="nf">remove</span><span class="p">()</span> <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>For testing:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">TestFlagManager</span><span class="p">(</span>
    <span class="k">private</span> <span class="kd">val</span> <span class="py">overrides</span><span class="p">:</span> <span class="nc">Map</span><span class="p">&lt;</span><span class="nc">Flag</span><span class="p">,</span> <span class="nc">Boolean</span><span class="p">&gt;</span> <span class="p">=</span> <span class="nf">emptyMap</span><span class="p">()</span>
<span class="p">)</span> <span class="p">:</span> <span class="nc">FlagManager</span> <span class="p">{</span>
    <span class="k">override</span> <span class="k">fun</span> <span class="nf">isEnabled</span><span class="p">(</span><span class="n">flag</span><span class="p">:</span> <span class="nc">Flag</span><span class="p">)</span> <span class="p">=</span> <span class="n">overrides</span><span class="p">[</span><span class="n">flag</span><span class="p">]</span> <span class="o">?:</span> <span class="n">flag</span><span class="p">.</span><span class="n">defaultValue</span>
    <span class="k">override</span> <span class="k">fun</span> <span class="nf">observe</span><span class="p">(</span><span class="n">flag</span><span class="p">:</span> <span class="nc">Flag</span><span class="p">)</span> <span class="p">=</span> <span class="nf">flowOf</span><span class="p">(</span><span class="nf">isEnabled</span><span class="p">(</span><span class="n">flag</span><span class="p">))</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Any class depending on <code class="language-plaintext highlighter-rouge">FlagManager</code> (the interface) is testable without touching Firebase:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Test</span>
<span class="k">fun</span> <span class="nf">`card</span> <span class="n">top</span> <span class="n">up</span> <span class="n">button</span> <span class="n">visible</span> <span class="k">when</span> <span class="n">flag</span> <span class="k">is</span> <span class="nf">enabled`</span><span class="p">()</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">flagManager</span> <span class="p">=</span> <span class="nc">TestFlagManager</span><span class="p">(</span><span class="n">overrides</span> <span class="p">=</span> <span class="nf">mapOf</span><span class="p">(</span><span class="nc">FeatureFlags</span><span class="p">.</span><span class="nc">CARD_TOP_UP</span> <span class="n">to</span> <span class="k">true</span><span class="p">))</span>
    <span class="kd">val</span> <span class="py">viewModel</span> <span class="p">=</span> <span class="nc">CardViewModel</span><span class="p">(</span><span class="n">flagManager</span><span class="p">,</span> <span class="o">..</span><span class="p">.)</span>
    <span class="c1">// Assert button visibility</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="the-startup-race-condition">The Startup Race Condition</h2>

<p>Remote flags are fetched from the network. Your app starts. You check a flag. The network call hasn’t completed. What do you return?</p>

<p>The naive answer — the default value — introduces measurement error in gradual rollouts. Users who hit the default in the first seconds get counted as “not in treatment” even if they should be.</p>

<p>Our solution: fetch with a timeout at startup, then proceed.</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">suspend</span> <span class="k">fun</span> <span class="nf">fetchFlagsWithFallback</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">try</span> <span class="p">{</span>
        <span class="nf">withTimeout</span><span class="p">(</span><span class="mi">2_000</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">remoteConfig</span><span class="p">.</span><span class="nf">fetchAndActivate</span><span class="p">().</span><span class="nf">await</span><span class="p">()</span>
        <span class="p">}</span>
    <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="n">e</span><span class="p">:</span> <span class="nc">TimeoutCancellationException</span><span class="p">)</span> <span class="p">{</span>
        <span class="c1">// Use cached values from last successful fetch — don't block startup</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Firebase Remote Config persists the last successful fetch. Two seconds is enough on reasonable connections without meaningfully delaying startup.</p>

<h2 id="getting-other-squads-to-adopt-it">Getting Other Squads to Adopt It</h2>

<p>The framework worked well for the Android team. Getting other squads to adopt the same flag infrastructure was the more interesting challenge.</p>

<p>I presented at a company-wide tech demo. The pitch: consistent kill-switch behaviour across mobile and web. The kill switch story landed better than the A/B testing story — because the kill switch is what saves your launch.</p>

<p>Good technical decisions don’t sell themselves. You have to show people why they care.</p>]]></content><author><name>Yousuf Sohail</name></author><category term="android" /><category term="development" /><category term="engineering" /><summary type="html"><![CDATA[Feature flags are one of those things every team says they want and few implement well.]]></summary></entry><entry><title type="html">Why I said yes to Nairobi</title><link href="https://yousufsohail.com/writing/why-i-joined-cashia/" rel="alternate" type="text/html" title="Why I said yes to Nairobi" /><published>2025-08-01T00:00:00+00:00</published><updated>2025-08-01T00:00:00+00:00</updated><id>https://yousufsohail.com/writing/why-i-joined-cashia</id><content type="html" xml:base="https://yousufsohail.com/writing/why-i-joined-cashia/"><![CDATA[<p>I get asked this. Why leave Tamara — a well-funded BNPL unicorn in Dubai — for Cashia, a smaller fintech headquartered in Nairobi?</p>

<p>The honest answer: it was exactly the kind of irrational, right-feeling decision that has shaped my career.</p>

<h2 id="the-setup">The Setup</h2>

<p>Two years at Tamara. Good work. Good team. Good compensation. The kind of role you could stay in for a long time and be comfortable.</p>

<p>I was getting comfortable.</p>

<p>When the Cashia opportunity came up, the role was Lead Mobile Engineer — technically lateral. Earlier stage company. KMM stack, which I’d evaluated at Delivery Hero but never shipped in production. Remote-first, with the core team in Nairobi.</p>

<p>On paper, the sensible move was to stay.</p>

<h2 id="why-i-said-yes">Why I Said Yes</h2>

<p>Three things.</p>

<p><strong>The problem felt real in a way that BNPL doesn’t.</strong></p>

<p>BNPL is a useful product. But it’s a product for people who already have access to finance — people with smartphones, data plans, purchasing power. It makes buying things slightly more convenient.</p>

<p>Cashia is building payments infrastructure for markets where meaningful portions of the population are unbanked. The problem isn’t “make shopping more convenient.” It’s “enable people to transact at all.” That’s a different category of problem.</p>

<p>I found I cared about the second category more than I’d realised.</p>

<p><strong>The KMM bet.</strong></p>

<p>I’d spent months at Delivery Hero evaluating KMM. I believed in the direction. But I’d never shipped it in a production app that real users depended on. Cashia was running KMM in production. Joining was a chance to learn from reality, not theory.</p>

<p>When the technical decision you’ve been thinking about theoretically is being stress-tested in production somewhere — go work there.</p>

<p><strong>Early-stage is different.</strong></p>

<p>At Delivery Hero and Tamara, I was one engineer among many. Impact was real but diffuse. At Cashia, I’d shape the platform from a much earlier point. Higher risk. Higher learning rate. Higher leverage.</p>

<h2 id="what-happened">What Happened</h2>

<p>I joined in July 2025. The team was building toward first public launch. No feature flag framework. No forced update mechanism. No mocking layer.</p>

<p>I built all three in the first two months.</p>

<p>Then: card top-up across mobile, backend, and payments. M-PESA cash-out. Request Money. A company-wide tech demo. A flight to Nairobi.</p>

<p>Seven months later, I was promoted to Engineering Manager.</p>

<h2 id="the-nairobi-part">The Nairobi Part</h2>

<p>When I told people I was joining a company based in Nairobi, some said: “Oh, remote from Dubai?” As if Nairobi was an abstraction.</p>

<p>I flew to Nairobi. Worked on-site with the team. Went to Nairobi National Park on a weekend and watched giraffes walk against the city skyline.</p>

<p>The engineers I worked with had the same arguments I have everywhere. Which state management pattern is better. Whether to abstract early or wait for duplication. How much technical debt is acceptable pre-launch.</p>

<p>The same arguments. The same craft. Different context — one that mattered.</p>

<p>That shift changed something in how I think about what we’re building.</p>

<h2 id="what-id-tell-someone-in-the-same-position">What I’d Tell Someone in the Same Position</h2>

<p>If you’re choosing between comfortable and interesting: choose interesting. Every time.</p>

<p>The career lessons that actually change you come from decisions that were slightly too big to feel comfortable at the time.</p>

<p>Comfortable is safe. Safe is slow.</p>]]></content><author><name>Yousuf Sohail</name></author><category term="career" /><category term="life" /><summary type="html"><![CDATA[I get asked this. Why leave Tamara — a well-funded BNPL unicorn in Dubai — for Cashia, a smaller fintech headquartered in Nairobi?]]></summary></entry><entry><title type="html">Which AI tools survived a year in my workflow</title><link href="https://yousufsohail.com/writing/ai-tools-android-workflow/" rel="alternate" type="text/html" title="Which AI tools survived a year in my workflow" /><published>2025-06-15T00:00:00+00:00</published><updated>2025-06-15T00:00:00+00:00</updated><id>https://yousufsohail.com/writing/ai-tools-android-workflow</id><content type="html" xml:base="https://yousufsohail.com/writing/ai-tools-android-workflow/"><![CDATA[<p>A year ago, I started deliberately integrating AI tools into my Android engineering workflow. Not to stay current, not because it was trendy — because I was curious whether any of it would actually change how fast I ship.</p>

<p>Here’s what stayed and what didn’t.</p>

<h2 id="the-honest-starting-point">The Honest Starting Point</h2>

<p>I was sceptical. I’d used GitHub Copilot. It autocompleted boilerplate acceptably and occasionally suggested something subtly wrong in a way that was worse than nothing. My prior experience: useful for CRUD, unreliable for anything nuanced.</p>

<p>That was 2023. A lot changed.</p>

<h2 id="what-actually-stayed">What Actually Stayed</h2>

<h3 id="llms-for-architecture-decisions">LLMs for Architecture Decisions</h3>

<p>I use Claude as a rubber duck for architecture discussions. Not to generate code — to think through problems.</p>

<p>When I was designing the feature flag framework at Cashia, I described the constraints: Android + KMM, needs remote and local override, engineers should add a new flag in one file. I asked Claude to identify failure modes in my proposed design.</p>

<p>It surfaced two things I’d missed: a race condition during startup where remote config might not have loaded and a flag returns a stale default; and a missing test interface that would couple unit tests to the remote config SDK unnecessarily.</p>

<p>I fixed both before writing production code.</p>

<p>The pattern: describe the problem and constraints, propose your solution, ask “what are the failure modes and what am I missing?” Different from “write my code” — it’s using LLMs for what they’re actually good at.</p>

<h3 id="pre-review-before-human-review">Pre-Review Before Human Review</h3>

<p>I use AI to pre-review my own PRs before requesting human review. I paste the diff, ask for: (1) logic errors, (2) things a reviewer will flag, (3) whether the change matches the commit message.</p>

<p>In practice, it catches roughly 60% of what human reviewers would flag — mostly missing null checks, inconsistent error handling, tests that don’t cover the unhappy path.</p>

<p>The other 40% — contextual knowledge, product implications, cross-team dependencies — it misses completely. That’s why humans still review. But arriving at review with the obvious issues already addressed makes reviews faster and more substantive.</p>

<h3 id="boilerplate-generation">Boilerplate Generation</h3>

<p><code class="language-plaintext highlighter-rouge">RecyclerView</code> adapters, <code class="language-plaintext highlighter-rouge">DiffUtil</code> implementations, ViewModel boilerplate, Room setup — deterministic enough that the output is usually right, tedious enough that generating manually is pure waste.</p>

<p>The rule: I read every line of AI-generated boilerplate before committing it. I’ve caught three incorrect suggestions in twelve months. None catastrophic. All would have caused bugs.</p>

<h2 id="what-didnt-work">What Didn’t Work</h2>

<p><strong>AI for business logic.</strong> This is where subtle errors live. The code looks correct, compiles, passes the happy path test, and fails in production under edge conditions requiring domain knowledge to anticipate. I’ve stopped using AI to generate business logic directly.</p>

<p><strong>IDE-integrated autocomplete for Kotlin.</strong> Too low-signal. Android Studio’s built-in completion is already quite good, and AI overlay consistently offered plausible-looking alternatives wrong for my context. Turned it off after six weeks.</p>

<p><strong>AI for debugging.</strong> I expected this to be useful. “Why is this crash happening?” In practice, AI doesn’t have your full context — database state, network responses, navigation back stack. It gives generic suggestions already in your checklist.</p>

<h2 id="the-honest-assessment">The Honest Assessment</h2>

<p>AI tools have made me roughly 15–20% faster on uninteresting work — boilerplate, documentation, pre-review. They’ve made me more rigorous about architecture because having a tool that surfaces failure modes changes how carefully you describe systems.</p>

<p>They haven’t replaced the hard parts. Understanding the Android threading model deeply enough to avoid ANRs, knowing when not to use a ViewModel, reading battery and memory profiles — these require accumulated experience that AI tools don’t have.</p>

<p>Use the tools. Don’t trust them blindly. Know what they’re actually good at.</p>]]></content><author><name>Yousuf Sohail</name></author><category term="android" /><category term="ai" /><category term="productivity" /><summary type="html"><![CDATA[A year ago, I started deliberately integrating AI tools into my Android engineering workflow. Not to stay current, not because it was trendy — because I was curious whether any of it would actually change how fast I ship.]]></summary></entry><entry><title type="html">Why we’re homeschooling</title><link href="https://yousufsohail.com/writing/homeschooling/" rel="alternate" type="text/html" title="Why we’re homeschooling" /><published>2024-11-20T00:00:00+00:00</published><updated>2024-11-20T00:00:00+00:00</updated><id>https://yousufsohail.com/writing/homeschooling</id><content type="html" xml:base="https://yousufsohail.com/writing/homeschooling/"><![CDATA[<p>My wife and I are homeschooling our kids.</p>

<p>Not because we’re anti-education. Because we’re pro-learning.</p>

<h2 id="the-current-system">The Current System</h2>

<p>Schools optimize for compliance, not curiosity.</p>

<p>Sit still. Don’t question. Memorize this. Test on Friday.</p>

<p>It worked for the industrial age. We needed factory workers.</p>

<p>We don’t live in that world anymore.</p>

<h2 id="what-we-want">What We Want</h2>

<p>Kids who think. Who question. Who create.</p>

<p>Kids who keep their fitrah - their natural state.</p>

<p>Kids who learn because they’re curious, not because they’re scared of failing.</p>

<h2 id="the-plan">The Plan</h2>

<p>We’re not recreating school at home. That misses the point.</p>

<p>Instead:</p>
<ul>
  <li><strong>Morning</strong>: Quran, Arabic, Islamic studies</li>
  <li><strong>Midday</strong>: Math and science through projects</li>
  <li><strong>Afternoon</strong>: Whatever they’re interested in</li>
</ul>

<p>Some days that’s coding. Some days it’s cooking. Some days it’s just playing outside.</p>

<h2 id="the-tech-angle">The Tech Angle</h2>

<p>I’m building tools as we need them:</p>
<ul>
  <li>Quran memorization tracker</li>
  <li>Math problems generator</li>
  <li>Science experiment logger</li>
</ul>

<p>Nothing fancy. Just solving our problems.</p>

<h2 id="the-challenges">The Challenges</h2>

<p>It’s not easy.</p>

<p>We doubt ourselves daily. Are we ruining their future? Are we being too idealistic?</p>

<p>But then I watch my kid spend three hours absorbed in how ants work. No bells. No schedule. Just pure learning.</p>

<p>That’s worth the uncertainty.</p>

<h2 id="resources-were-using">Resources We’re Using</h2>

<ul>
  <li>Khan Academy (math)</li>
  <li>Quran.com (Quran and Arabic)</li>
  <li>YouTube (everything else)</li>
  <li>The outdoors (the best classroom)</li>
</ul>

<h2 id="early-results">Early Results</h2>

<p>My eldest can focus for hours on things that interest him.</p>

<p>He asks “why” constantly.</p>

<p>He’s not afraid of being wrong.</p>

<p>These feel like wins.</p>

<h2 id="the-long-game">The Long Game</h2>

<p>We’re not trying to create prodigies.</p>

<p>We’re trying to raise humans who think, who care, who contribute.</p>

<p>If they end up in traditional school later, fine. But they’ll enter with curiosity intact.</p>

<h2 id="for-other-parents">For Other Parents</h2>

<p>If you’re considering this:</p>

<ol>
  <li>You don’t need to be a teacher</li>
  <li>You don’t need a curriculum (at first)</li>
  <li>You do need patience</li>
  <li>You do need to trust the process</li>
</ol>

<p>Start small. Even one day a week. See how it feels.</p>

<hr />

<p><em>Education is not filling a bucket, but lighting a fire. We’re just trying to keep the fire burning.</em></p>]]></content><author><name>Yousuf Sohail</name></author><category term="life" /><category term="family" /><summary type="html"><![CDATA[My wife and I are homeschooling our kids.]]></summary></entry><entry><title type="html">The art of vibe coding</title><link href="https://yousufsohail.com/writing/vibe-coding/" rel="alternate" type="text/html" title="The art of vibe coding" /><published>2024-11-15T00:00:00+00:00</published><updated>2024-11-15T00:00:00+00:00</updated><id>https://yousufsohail.com/writing/vibe-coding</id><content type="html" xml:base="https://yousufsohail.com/writing/vibe-coding/"><![CDATA[<p>I’ve been coding professionally for 12 years. Followed every methodology - Agile, Scrum, TDD, DDD, whatever-DD. But lately, I’ve been doing something different.</p>

<p>I call it vibe coding.</p>

<h2 id="whats-vibe-coding">What’s Vibe Coding?</h2>

<p>It’s coding by feel. Following your intuition instead of a rigid plan. Starting with what excites you, not what’s “most important” on the backlog.</p>

<p>Sounds unprofessional? Maybe. But here’s what I’ve noticed:</p>

<p>When I vibe code, I ship faster. The code is cleaner. I’m happier.</p>

<h2 id="the-process-if-you-can-call-it-that">The Process (If You Can Call It That)</h2>

<ol>
  <li>
    <p><strong>Start with energy, not priority</strong><br />
Work on what pulls you. That tricky animation? That refactor that’s been bugging you? Start there.</p>
  </li>
  <li>
    <p><strong>Follow the thread</strong><br />
One thing leads to another. Fixing the animation reveals a state management issue. Fixing that improves the whole architecture.</p>
  </li>
  <li>
    <p><strong>Stop when it stops flowing</strong><br />
When the vibe dies, stop. Switch to something else or take a break. Forcing it produces bad code.</p>
  </li>
</ol>

<h2 id="why-it-works">Why It Works</h2>

<p>Our brains are pattern-matching machines. After years of coding, your intuition knows things your conscious mind doesn’t.</p>

<p>That “feeling” that something’s wrong? That’s years of experience talking.</p>

<p>That excitement about a particular approach? That’s your brain recognizing a good pattern.</p>

<h2 id="when-to-vibe-code">When to Vibe Code</h2>

<ul>
  <li>Side projects (always)</li>
  <li>Exploration and prototyping</li>
  <li>Refactoring sessions</li>
  <li>When you’re stuck on a problem</li>
</ul>

<h2 id="when-not-to-vibe-code">When NOT to Vibe Code</h2>

<ul>
  <li>Production hotfixes</li>
  <li>Team projects with tight deadlines</li>
  <li>When learning something completely new</li>
  <li>When you’re tired (tired vibes lie)</li>
</ul>

<h2 id="the-balance">The Balance</h2>

<p>I’m not saying abandon all structure. At work, we have sprints and deadlines. But within that structure, I find space to vibe.</p>

<p>Monday morning? I’ll tackle that payment flow refactor I’m excited about.</p>

<p>Thursday afternoon when I’m dragging? I’ll do the boring ticket.</p>

<h2 id="results">Results</h2>

<p>Since I started vibe coding:</p>
<ul>
  <li>Built a side project in a weekend that I’d been overthinking for weeks</li>
  <li>Solved three “impossible” bugs by following hunches</li>
  <li>Actually enjoyed refactoring (who knew?)</li>
  <li>Code reviews got better - “This feels really clean”</li>
</ul>

<h2 id="try-it">Try It</h2>

<p>Next time you sit down to code, ask yourself: What am I drawn to right now?</p>

<p>Follow that feeling. See where it leads.</p>

<p>You might be surprised.</p>

<hr />

<p><em>Sometimes the best methodology is no methodology. Trust your instincts. They’ve been trained by every line of code you’ve ever written.</em></p>]]></content><author><name>Yousuf Sohail</name></author><category term="development" /><summary type="html"><![CDATA[I’ve been coding professionally for 12 years. Followed every methodology - Agile, Scrum, TDD, DDD, whatever-DD. But lately, I’ve been doing something different.]]></summary></entry></feed>