<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Kirera paul murithi</title>
    <description>The latest articles on DEV Community by Kirera paul murithi (@paulmurithi).</description>
    <link>https://dev.to/paulmurithi</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1242848%2Ffd1c82cd-f6d7-4a98-85df-e3983ce038c0.jpeg</url>
      <title>DEV Community: Kirera paul murithi</title>
      <link>https://dev.to/paulmurithi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/paulmurithi"/>
    <language>en</language>
    <item>
      <title>Shifting Left: How TDD Became the Foundation of SokoFlow's Core Engine</title>
      <dc:creator>Kirera paul murithi</dc:creator>
      <pubDate>Tue, 30 Jun 2026 12:35:46 +0000</pubDate>
      <link>https://dev.to/paulmurithi/shifting-left-how-tdd-became-the-foundation-of-sokoflows-core-engine-485f</link>
      <guid>https://dev.to/paulmurithi/shifting-left-how-tdd-became-the-foundation-of-sokoflows-core-engine-485f</guid>
      <description>&lt;h3&gt;
  
  
  SokoFlow Build Log — Month 1 of 4
&lt;/h3&gt;

&lt;p&gt;Last semester I set out on a new strategic plan to level up my software development skills through deliberate, project-based learning. That work produced one of the most ambitious things I've built so far: &lt;strong&gt;Sim-Pesa&lt;/strong&gt;, a local-first transactional appliance that lets developers working in the M-Pesa ecosystem test and simulate STK Push workflows entirely on their own machines, without depending on the Daraja sandbox. I documented that build in 16 weekly posts, which you can find &lt;a href="https://hashnode.com/@paul-murithi" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This semester, the focus shifts — from fintech foundations to cloud-native integration and real-world systems. The flagship project is &lt;strong&gt;SokoFlow&lt;/strong&gt;, a conversational ERP for small Kenyan shopkeepers to track inventory and record sales entirely through WhatsApp chat. No app to download, no training session required — just natural language.&lt;/p&gt;

&lt;p&gt;Where Sim-Pesa lived in a controlled, predictable transactional world, SokoFlow steps into the mess of cloud-native reality: third-party API failures, webhook signature verification, the statelessness of HTTP, and container orchestration. The target audience shifts too — Kenyan SMEs operating on infrastructure that is often unreliable by design, not by exception.&lt;/p&gt;

&lt;p&gt;It's an ambitious project, but the goal was always to learn as much as possible from it. With the plan in place, I got to work.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The Vision of a Headless ERP
&lt;/h2&gt;

&lt;p&gt;The first real question I had to answer before writing a line of code: what does "headless" actually mean?&lt;/p&gt;

&lt;p&gt;Headless architecture decouples the frontend — the "head," or user interface — from the backend, the "body" that holds the data and business logic. A conventional ERP bundles both: backend plus a dashboard or UI on top. A headless ERP, by contrast, is just the engine. The brain. There's no built-in screen.&lt;/p&gt;

&lt;p&gt;So how do users interact with a system that has no interface of its own? SokoFlow doesn't actually care. It could be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;WhatsApp&lt;/li&gt;
&lt;li&gt;SMS&lt;/li&gt;
&lt;li&gt;A web app&lt;/li&gt;
&lt;li&gt;A mobile app&lt;/li&gt;
&lt;li&gt;A voice assistant&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this case, the "frontend" happens to be a WhatsApp conversation. Instead of clicking "Add Product," the shopkeeper just texts:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Added 5 packets of milk"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And the backend processes that message the way an ERP would process a structured command.&lt;/p&gt;

&lt;p&gt;In a sense, most modern systems are already headless-ish. Any architecture where the backend exposes an API and the frontend simply consumes JSON over HTTP is practically headless by default — if you've built something like that before, you were already doing this without naming it.&lt;/p&gt;

&lt;p&gt;SokoFlow just makes the principle explicit. The backend is built on the assumption that it may &lt;em&gt;never&lt;/em&gt; have a traditional UI:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Normal web app&lt;/th&gt;
&lt;th&gt;SokoFlow&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Backend exists mainly to serve a website or app&lt;/td&gt;
&lt;td&gt;Backend &lt;strong&gt;is&lt;/strong&gt; the product&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frontend is the product&lt;/td&gt;
&lt;td&gt;WhatsApp is just one client talking to it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That framing matters because it means tomorrow I could plug in Telegram, SMS, a voice bot, a React dashboard, or USSD — without touching the core business logic.&lt;/p&gt;

&lt;p&gt;Month 1's task wasn't the exciting part on the surface: four weeks spent entirely on core business logic, with no async layers and no WhatsApp integration in sight. That was deliberate. The core is everything — if it's wrong, the conversation engine built on top of it will be wrong too. That focus is also what pulled me into one of the most talked-about (and most misunderstood) practices in software engineering: &lt;strong&gt;Test-Driven Development&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The Power of TDD in Core Logic
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Test-driven_development" rel="noopener noreferrer"&gt;TDD&lt;/a&gt; is a development style where you write a failing automated test first, write just enough code to make it pass, then refactor both the test and the implementation — and repeat for the next piece of behavior. This was my first real experience working this way, and it initially felt backwards. Most of us default to Design → Write Code → Write Tests. TDD inverts that order entirely.&lt;/p&gt;

&lt;p&gt;Once it clicked, though, it was genuinely simple — the cycle is known as &lt;strong&gt;Red-Green-Refactor&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Red — write a failing test.&lt;/strong&gt; Define exactly what a piece of code should do before it exists. Since only the test exists, it fails by definition.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Green — write just enough code.&lt;/strong&gt; The minimum implementation required to make that test pass. Nothing more.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refactor — clean it up.&lt;/strong&gt; Revisit both the test and the implementation, tighten them up, and confirm the tests still pass.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the entire playbook, repeated feature by feature. After working in this loop for a few weeks, I came around to it completely — it forces you to think through edge cases and the shape of a request-response cycle &lt;em&gt;before&lt;/em&gt; you write the implementation, not after.&lt;/p&gt;

&lt;p&gt;With that approach set, here's how the four weeks broke down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Week 1 — Project scaffold:&lt;/strong&gt; PostgreSQL schema, Alembic migrations, FastAPI skeleton.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Week 2 — Product core:&lt;/strong&gt; 20+ unit tests covering product CRUD operations (add, update, delete, and friends).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Week 3 — Inventory management:&lt;/strong&gt; Inventory deduction logic tested against boundary cases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Week 4 — Sales recording &amp;amp; daily aggregation:&lt;/strong&gt; A sales service backed by report queries that return accurate totals against test data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By the end of the month: &lt;strong&gt;40+ tests, 92% coverage.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The Technical Hurdles
&lt;/h2&gt;

&lt;p&gt;Everything moved smoothly on the surface, but the real lessons — as always — came from what went wrong along the way.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Wall of New Tooling
&lt;/h3&gt;

&lt;p&gt;This was my first extended stretch writing Python, which meant getting fluent in what I've started calling the "modern Python stack."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Type hints.&lt;/strong&gt; In old-style Python, &lt;code&gt;def greet(name):&lt;/code&gt; tells you nothing about what &lt;code&gt;name&lt;/code&gt; actually is — a string, a list, a database object — until the code crashes at runtime. Adding type hints like &lt;code&gt;name: str&lt;/code&gt; makes the expectation explicit, both for other developers and for tooling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MyPy — the quality inspector.&lt;/strong&gt; Python itself ignores type hints at runtime; they're purely documentation unless something enforces them. MyPy is a static analysis tool that reads code without executing it, catching type errors that would otherwise slip through and surface only in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pydantic — the bouncer.&lt;/strong&gt; FastAPI is built on Pydantic, a data validation library. If type hints are documentation, Pydantic is enforcement: it uses those same type hints to validate that data entering the application — from users, APIs, or the database — is exactly what it claims to be.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fighting Async Testing and DB Drivers
&lt;/h3&gt;

&lt;p&gt;Going all-in on TDD meant spending a lot of time inside the test suite, which meant the environment had to be low-friction enough to iterate quickly. The center of that effort was a single file: &lt;code&gt;conftest.py&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Understanding &lt;code&gt;conftest.py&lt;/code&gt; and fixtures.&lt;/strong&gt; Before pytest's fixture system existed, test setup was a maintenance headache — every test file needed the same boilerplate (database connections, authenticated users, HTTP clients), duplicated across the suite. Any change meant updating dozens of files, and forgotten cleanup could quietly break unrelated tests. &lt;code&gt;conftest.py&lt;/code&gt; exists to centralize that shared setup in one place. Pytest discovers it automatically and makes everything inside available to every test in the directory tree, with no explicit imports required.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;fixture&lt;/strong&gt; is just a function that builds whatever a test needs, hands it over with &lt;code&gt;yield&lt;/code&gt;, and guarantees cleanup afterward — even if the test fails. That lets each test focus purely on its assertions. Pytest also lets fixtures live at different scopes, from a fresh instance per test (&lt;code&gt;function&lt;/code&gt;) to a single shared instance for the whole run (&lt;code&gt;session&lt;/code&gt;); the goal is to use the broadest scope that still keeps tests properly isolated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Moving from sync to async changed more than a few keywords.&lt;/strong&gt; Swapping SQLite and standard SQLAlchemy sessions for &lt;code&gt;asyncpg&lt;/code&gt;, &lt;code&gt;AsyncSession&lt;/code&gt;, and async route handlers introduces an event loop — and every async resource is tied to the loop that created it. By default, &lt;code&gt;pytest-asyncio&lt;/code&gt; spins up a fresh event loop per test, which can leave long-lived objects like the database engine bound to a loop that no longer exists. The fix was a session-scoped &lt;code&gt;event_loop&lt;/code&gt; fixture so the entire suite shares one consistent loop.&lt;/p&gt;

&lt;p&gt;That wasn't the only wrinkle. SQLAlchemy's default connection pooling — great in production — can leak state between tests and cause loop-ownership conflicts, so switching to &lt;code&gt;NullPool&lt;/code&gt; ensures every connection is opened, used, and immediately discarded. FastAPI's synchronous &lt;code&gt;TestClient&lt;/code&gt; also has to bridge sync and async code, which made loop issues more likely; switching to &lt;code&gt;httpx.AsyncClient&lt;/code&gt; kept the tests, the client, and the application running on the same event loop, resulting in a far more reliable setup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Time and Timezones
&lt;/h3&gt;

&lt;p&gt;This trips up even experienced developers, and it caught up with me too — starting with not fully internalizing the difference between &lt;strong&gt;naive&lt;/strong&gt; and &lt;strong&gt;aware&lt;/strong&gt; datetimes.&lt;/p&gt;

&lt;p&gt;A naive datetime — &lt;code&gt;datetime(2026, 6, 25, 15, 0)&lt;/code&gt; — just says "3 PM." But 3 PM &lt;em&gt;where&lt;/em&gt;? London? Nairobi? There's no way to know. An aware datetime — &lt;code&gt;datetime(2026, 6, 25, 15, tzinfo=UTC)&lt;/code&gt; — says "3 PM UTC," which is complete information.&lt;/p&gt;

&lt;p&gt;Picture two servers, one in Kenya and one in New York, both calling &lt;code&gt;datetime.now()&lt;/code&gt;. The Kenya server returns 15:00; the New York server returns 08:00. Same moment, different values — which is exactly the kind of inconsistency that makes naive datetimes unsafe in production. The fix is &lt;code&gt;datetime.now(UTC)&lt;/code&gt;, so every server agrees on a single source of truth.&lt;/p&gt;

&lt;p&gt;The sharpest version of this problem showed up in the daily report logic. To answer "what did this shop sell today," the database needs the start and end of &lt;em&gt;that specific day&lt;/em&gt; — but in &lt;em&gt;which&lt;/em&gt; timezone?&lt;/p&gt;

&lt;p&gt;The shop operates in Kenya, on EAT (UTC+3) year-round. If an owner asks for the report for June 20, they mean everything between midnight and 11:59:59 PM on June 20, &lt;strong&gt;Kenya time&lt;/strong&gt;. In UTC, those boundaries are:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Local (EAT)&lt;/th&gt;
&lt;th&gt;UTC&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Jun 20 00:00&lt;/td&gt;
&lt;td&gt;Jun 19 21:00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jun 21 00:00&lt;/td&gt;
&lt;td&gt;Jun 20 21:00&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So the correct query is &lt;code&gt;created_at &amp;gt;= 2025-06-19T21:00:00Z AND created_at &amp;lt; 2025-06-20T21:00:00Z&lt;/code&gt; — notice that a single "June 20" local day actually spans two different UTC calendar dates.&lt;/p&gt;

&lt;p&gt;My initial implementation got this wrong by treating the date naively:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;UTC&lt;/th&gt;
&lt;th&gt;Kenya (UTC+3)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Jun 20 00:00&lt;/td&gt;
&lt;td&gt;Jun 20 03:00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jun 21 00:00&lt;/td&gt;
&lt;td&gt;Jun 21 03:00&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That version was effectively collecting sales from 3 AM June 20 to 3 AM June 21 Kenya time, instead of midnight to midnight. The result: sales between midnight and 3 AM disappeared from the correct day's report, and late-night sales from the &lt;em&gt;next&lt;/em&gt; day bled into it.&lt;/p&gt;

&lt;p&gt;If a shop closes at 8 PM, this bug is invisible — almost nothing happens between midnight and 3 AM, so the totals come out right by coincidence. But the moment that assumption breaks — late opening hours, an online order at 1 AM, an overnight automated payment, an inventory sync that runs after midnight — the cracks show immediately: June 20's report comes up short, June 21's report has mysterious extra sales, and the daily totals stop matching the receipts.&lt;/p&gt;

&lt;p&gt;The rule of thumb that came out of this: when a user requests a report for a calendar day, interpret that date in the shop's local timezone, compute the local start and end of that day, convert those instants to UTC, and only then query the database (which stores everything in UTC). The underlying principle is that &lt;strong&gt;dates are a local concept; timestamps are absolute instants&lt;/strong&gt; — a "day" has to be defined in local time first, then translated to UTC for storage and querying.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Looking Ahead to Month 2: Docker, CI/CD, and Infrastructure Automation
&lt;/h2&gt;

&lt;p&gt;With Month 1 behind me and the core business logic locked down under a solid test suite, Month 2 shifts focus from a local code project to production-ready, cloud-native infrastructure. Three things are top of mind for the next four weeks:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Production-grade containerization.&lt;/strong&gt; Orchestrating SokoFlow's full topology with Docker — a multi-stage &lt;code&gt;docker-compose.yml&lt;/code&gt; that cleanly networks the FastAPI gateway, PostgreSQL 15, Redis 7, and a split Celery worker pool (&lt;code&gt;conversation_tasks&lt;/code&gt; and &lt;code&gt;report_tasks&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Automated CI/CD quality gates.&lt;/strong&gt; A strict GitHub Actions pipeline that runs the full test suite on every push and blocks pull requests if coverage drops below 85% or MyPy flags any strict type violations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Environment-aware configuration.&lt;/strong&gt; Hardening configuration management so that switching between development, staging, and production happens cleanly through environment variables, with no changes to application logic required.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Conclusion &amp;amp; Key Takeaway
&lt;/h2&gt;

&lt;p&gt;Finishing Month 1 confirmed one thing for me: &lt;strong&gt;build the business core before letting any external infrastructure distract you.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Forcing myself into strict TDD from day one surfaced complex boundary cases early — inventory hitting exactly zero, low-stock threshold triggers — without the noise of webhooks, servers, or third-party APIs in the way. Fighting through async test fixtures and timezone mismatches wasn't fun in the moment, but resolving those foundational issues now means Month 2 starts on an airtight, predictable core that's actually ready to scale.&lt;/p&gt;

&lt;p&gt;SokoFlow has its engine. Now it's time to build the container that runs it. Stay locked in for the next build log.&lt;/p&gt;

</description>
      <category>fastapi</category>
      <category>backenddevelopment</category>
      <category>buildinpublic</category>
      <category>postgressql</category>
    </item>
  </channel>
</rss>
