<?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: Lakshmi Venkatesan</title>
    <description>The latest articles on DEV Community by Lakshmi Venkatesan (@lakshmi_venkatesan_ad4fe9).</description>
    <link>https://dev.to/lakshmi_venkatesan_ad4fe9</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%2F4111568%2F333be06a-49b9-4fc9-b5e8-607d051951e1.jpg</url>
      <title>DEV Community: Lakshmi Venkatesan</title>
      <link>https://dev.to/lakshmi_venkatesan_ad4fe9</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lakshmi_venkatesan_ad4fe9"/>
    <language>en</language>
    <item>
      <title>I Built a Crash-Safe Database From Scratch Because I Wasn't Allowed to pip install Anything</title>
      <dc:creator>Lakshmi Venkatesan</dc:creator>
      <pubDate>Sat, 05 Sep 2026 19:44:53 +0000</pubDate>
      <link>https://dev.to/lakshmi_venkatesan_ad4fe9/i-built-a-crash-safe-database-from-scratch-because-i-wasnt-allowed-to-pip-install-anything-3d5c</link>
      <guid>https://dev.to/lakshmi_venkatesan_ad4fe9/i-built-a-crash-safe-database-from-scratch-because-i-wasnt-allowed-to-pip-install-anything-3d5c</guid>
      <description>&lt;p&gt;When I saw the rule for Zero Dependency 2026 — &lt;em&gt;"your dependency manifest must be empty"&lt;/em&gt; — my first reaction was honestly panic. For most small storage problems, my default choice is &lt;code&gt;pip install tinydb&lt;/code&gt; and move on. Track D (Data &amp;amp; Storage) meant I had to actually build the thing TinyDB gives you for free, using nothing but the Python standard library.&lt;/p&gt;

&lt;p&gt;So that's what I did. The result is &lt;strong&gt;ChronicleKV&lt;/strong&gt; — an embedded key-value store with a real write-ahead log, crash recovery, and point-in-time history. Here's what it actually took.&lt;/p&gt;

&lt;h2&gt;
  
  
  The package I normally reach for
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;TinyDB.&lt;/strong&gt; It's the obvious choice for small local document storage — dead simple API, no server, just works. But once I actually sat down to think about &lt;em&gt;why&lt;/em&gt; it's simple, I realized it's simple because it just dumps a JSON file to disk. There's no write-ahead log, no checksum, nothing. If a write gets interrupted at the wrong moment — process killed, power loss — you can end up with a JSON file that no longer parses cleanly, which can put the whole database at risk depending on when the interruption hit. Not something that happens on every crash, but a real gap with no recovery path built in.&lt;/p&gt;

&lt;p&gt;That's the exact problem I decided to solve instead of just reimplementing TinyDB's API for the sake of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually built instead
&lt;/h2&gt;

&lt;p&gt;The core is a &lt;strong&gt;write-ahead log (WAL)&lt;/strong&gt;. Every write gets appended as a binary record — I used &lt;code&gt;struct.pack&lt;/code&gt;/&lt;code&gt;struct.unpack&lt;/code&gt; for the encoding, with a fixed header (magic bytes, version, operation type, sequence number, timestamp, key length, value length) followed by the actual key/value bytes, followed by a &lt;strong&gt;CRC32 checksum&lt;/strong&gt; computed with &lt;code&gt;zlib.crc32&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The checksum is the whole point. On startup, ChronicleKV replays the log from the beginning. If a record's CRC doesn't match its bytes, that's the exact point where the process died mid-write — everything before it is valid, everything from that point on gets truncated and discarded. No corrupted file, no unreadable database. Just "recovery stopped at the last good write."&lt;/p&gt;

&lt;p&gt;I didn't trust myself on this, so I built &lt;code&gt;chronicle crash-demo-compare&lt;/code&gt; — it actually forks a writer process, kills it mid-flight at random points, and counts what survived (numbers below). Sync mode gets that guarantee because every write is &lt;code&gt;fsync&lt;/code&gt;'d before it's acknowledged; async mode buffers writes and only flushes on a trigger, which is where the loss comes from.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually took
&lt;/h2&gt;

&lt;p&gt;Some numbers, since claims like "it works" mean more with something to check them against:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WAL + recovery logic&lt;/strong&gt;: ~187 lines (&lt;code&gt;wal.py&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage engine&lt;/strong&gt; (indexing, compaction, durability modes): ~413 lines (&lt;code&gt;store.py&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CLI&lt;/strong&gt;: ~261 lines (&lt;code&gt;cli.py&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Record overhead&lt;/strong&gt;: the binary header is 30 bytes (&lt;code&gt;struct.calcsize("&amp;gt;4sBBQQII")&lt;/code&gt; — magic, version, op, sequence, timestamp, key length, value length), plus a 4-byte CRC32 checksum. So every write costs 34 bytes of fixed overhead before the actual key/value bytes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tests&lt;/strong&gt;: 51, all passing — covering WAL encode/decode, recovery, compaction, durability modes, and edge cases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Crash-loss numbers&lt;/strong&gt;: sync mode, 0 lost writes across every run I tried; async mode, averaging 37–50 lost writes per mid-flight kill, depending on buffer state at the moment of the crash.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time&lt;/strong&gt;: ~18 hours across the 72-hour window.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;TinyDB gives you a simple JSON document store with fast reads but no durability guarantee beyond "the file happens to still be valid JSON." ChronicleKV trades a bit of write throughput and API simplicity for crash consistency and point-in-time history — that's the actual engineering trade-off, not just "TinyDB bad, ChronicleKV good."&lt;/p&gt;

&lt;h2&gt;
  
  
  Other packages I ended up not needing
&lt;/h2&gt;

&lt;p&gt;Once I was deep into this, I noticed how many small dependencies I would have normally reached for without thinking:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;click&lt;/code&gt;/&lt;code&gt;typer&lt;/code&gt; → &lt;code&gt;argparse&lt;/code&gt;.&lt;/strong&gt; I always assumed argparse was clunky. Turns out subparsers handle a full CLI (&lt;code&gt;chronicle get&lt;/code&gt;, &lt;code&gt;chronicle diff&lt;/code&gt;, &lt;code&gt;chronicle timeline&lt;/code&gt;, etc.) just fine once you actually sit with the docs for twenty minutes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;filelock&lt;/code&gt; → &lt;code&gt;fcntl.flock&lt;/code&gt;.&lt;/strong&gt; Needed this to enforce single-writer semantics on POSIX. Learned that Windows doesn't really have an equivalent that's as clean, so on Windows I fall back to trusting the process — documented that limitation instead of pretending it doesn't exist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;orjson&lt;/code&gt; → &lt;code&gt;json&lt;/code&gt;.&lt;/strong&gt; No surprises here, just used the standard module for the TinyDB-compatibility layer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;pytest&lt;/code&gt; → &lt;code&gt;unittest&lt;/code&gt;.&lt;/strong&gt; Honestly the hardest adjustment. I missed fixtures and parametrize immediately. But &lt;code&gt;unittest.TestCase&lt;/code&gt; gets you 90% of the way there — see the test count above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;diskcache&lt;/code&gt; → a plain dict.&lt;/strong&gt; The in-memory index is just &lt;code&gt;{key: offset_in_file}&lt;/code&gt;. The WAL file is the actual source of truth; the dict is just so lookups aren't O(n) scans.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The feature I'm actually proud of
&lt;/h2&gt;

&lt;p&gt;Because every write is a sequenced, appended record instead of an in-place mutation, I got something for free that I didn't originally plan for: &lt;strong&gt;time travel&lt;/strong&gt;. &lt;code&gt;db.get_at(key, seq=42)&lt;/code&gt; returns whatever the value was at that exact point in the log. &lt;code&gt;chronicle diff &amp;lt;file&amp;gt; &amp;lt;key&amp;gt; --from 4 --to 6&lt;/code&gt; shows you what changed between two points, and &lt;code&gt;chronicle timeline&lt;/code&gt; prints every write across the entire store in order — basically &lt;code&gt;git log&lt;/code&gt; for your data.&lt;/p&gt;

&lt;p&gt;None of that was the assignment. It just fell out of designing the storage format around an append-only log instead of trying to bolt "history" on top of a mutable file later.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell someone starting this track
&lt;/h2&gt;

&lt;p&gt;Don't think of "zero dependency" as a restriction you're working around — it's forcing you to actually understand what the dependency was &lt;em&gt;for&lt;/em&gt;. I always knew TinyDB was "simple" but I'd never actually thought about what durability guarantees it was quietly &lt;em&gt;not&lt;/em&gt; giving me until I had to write the fsync calls myself.&lt;/p&gt;

&lt;p&gt;Repo's here if you want to poke at the WAL format or the crash demo: &lt;strong&gt;&lt;a href="https://github.com/lakshmiv3322/chroniclekv" rel="noopener noreferrer"&gt;github.com/lakshmiv3322/chroniclekv&lt;/a&gt;&lt;/strong&gt; — there's a one-click Colab demo in the README if you want to run it without installing anything locally.&lt;/p&gt;

</description>
      <category>python</category>
      <category>database</category>
      <category>hackathon</category>
      <category>zerodependency</category>
    </item>
  </channel>
</rss>
