<?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: arfaaa243</title>
    <description>The latest articles on DEV Community by arfaaa243 (@arfaaa243).</description>
    <link>https://dev.to/arfaaa243</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%2F4109020%2F297854f0-8d54-49a6-8d92-a16945b2d3e4.png</url>
      <title>DEV Community: arfaaa243</title>
      <link>https://dev.to/arfaaa243</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/arfaaa243"/>
    <language>en</language>
    <item>
      <title>I Built a Crash-Safe Key-Value Store Without sqlite3 — Then I Killed It</title>
      <dc:creator>arfaaa243</dc:creator>
      <pubDate>Fri, 04 Sep 2026 05:09:53 +0000</pubDate>
      <link>https://dev.to/arfaaa243/i-built-a-crash-safe-key-value-store-without-sqlite3-then-i-killed-it-45a3</link>
      <guid>https://dev.to/arfaaa243/i-built-a-crash-safe-key-value-store-without-sqlite3-then-i-killed-it-45a3</guid>
      <description>&lt;p&gt;I built &lt;strong&gt;Anvil&lt;/strong&gt;, a crash-safe, embedded, log-structured key-value store for Hackathon Raptors' Zero Dependency Hackathon 2026 — Track D: Data &amp;amp; Storage.&lt;/p&gt;

&lt;p&gt;The constraint was simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No third-party runtime dependencies.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;But I didn't want to solve that by wrapping &lt;code&gt;sqlite3&lt;/code&gt; and calling it a storage engine.&lt;/p&gt;

&lt;p&gt;So I built the storage layer myself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;append-only log&lt;/li&gt;
&lt;li&gt;binary record format&lt;/li&gt;
&lt;li&gt;in-memory index&lt;/li&gt;
&lt;li&gt;CRC32 integrity checks&lt;/li&gt;
&lt;li&gt;crash recovery&lt;/li&gt;
&lt;li&gt;single-writer locking&lt;/li&gt;
&lt;li&gt;atomic compaction&lt;/li&gt;
&lt;li&gt;verification tooling&lt;/li&gt;
&lt;li&gt;reproducible single-file builds&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All with Python's standard library.&lt;/p&gt;

&lt;p&gt;The interesting part wasn't making this work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;put(key, value)
get(key)
delete(key)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interesting part was answering a harder question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What should a storage engine believe after the process dies halfway through a write?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's where Anvil really started.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why build another key-value store?
&lt;/h2&gt;

&lt;p&gt;There are already excellent storage options in Python.&lt;/p&gt;

&lt;p&gt;You can use &lt;code&gt;sqlite3&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You can use &lt;code&gt;shelve&lt;/code&gt; or &lt;code&gt;dbm&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You can install something like &lt;code&gt;diskcache&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So why build another one?&lt;/p&gt;

&lt;p&gt;Because Track D wasn't really asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Can you make a dictionary that persists?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It was asking what happens when you &lt;strong&gt;own the storage layer&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That meant Anvil couldn't simply hide persistence behind another database engine.&lt;/p&gt;

&lt;p&gt;I wanted every important guarantee to be visible in the code:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;write → persist → recover → verify → compact&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No black box.&lt;/p&gt;




&lt;h1&gt;
  
  
  The first design decision: append, don't overwrite
&lt;/h1&gt;

&lt;p&gt;The core of Anvil is an append-only log.&lt;/p&gt;

&lt;p&gt;Instead of finding an old value on disk and modifying it in place, every operation becomes a new record.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PUT user → Alice
PUT user → Bob
DELETE user
PUT user → Charlie
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The log contains the history.&lt;/p&gt;

&lt;p&gt;The in-memory index tells Anvil where the latest record for each key lives.&lt;/p&gt;

&lt;p&gt;So the architecture becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;             CLI / Python API
                    │
                    ▼
               ┌─────────┐
               │  Store  │
               └────┬────┘
                    │
             ┌──────┴──────┐
             ▼             ▼
       In-memory Index   Log File
                           │
                           ▼
                    Binary Records
                           │
                           ▼
                      CRC32 Check
                           │
                           ▼
                         Disk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This shape gives us something important:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;the index is not the source of truth.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The persisted log is.&lt;/p&gt;

&lt;p&gt;If the process disappears, Anvil can scan the log again and rebuild the index.&lt;/p&gt;

&lt;p&gt;That's a much more useful property for a crash-safe storage engine than simply keeping a dictionary in memory.&lt;/p&gt;




&lt;h1&gt;
  
  
  What does a record actually look like?
&lt;/h1&gt;

&lt;p&gt;I didn't use &lt;code&gt;pickle&lt;/code&gt; or an external serialization package.&lt;/p&gt;

&lt;p&gt;Anvil uses Python's &lt;code&gt;struct&lt;/code&gt; module to define its own binary record format.&lt;/p&gt;

&lt;p&gt;Conceptually, a record looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────┬─────────┬─────────┬────┬─────┬─────┬───────┐
│ checksum │ key_len │ val_len │ op │ seq │ key │ value │
│  4 bytes │ 4 bytes │ 4 bytes │ 1B │ 8B  │ ... │  ...  │
└──────────┴─────────┴─────────┴────┴─────┴─────┴───────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The checksum is generated with &lt;code&gt;zlib.crc32&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The sequence number gives records an ordering.&lt;/p&gt;

&lt;p&gt;The lengths tell the reader exactly how to split the key and value.&lt;/p&gt;

&lt;p&gt;And those length fields turned out to be more important than I initially expected.&lt;/p&gt;




&lt;h1&gt;
  
  
  The subtle corruption case
&lt;/h1&gt;

&lt;p&gt;A checksum sounds simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Calculate a checksum of the data and reject it if the checksum doesn't match.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But &lt;strong&gt;what exactly counts as "the data"?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Suppose the checksum only covered:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;key + value
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now imagine someone changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;key_len -= 1
val_len += 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The total body size can remain exactly the same.&lt;/p&gt;

&lt;p&gt;The underlying bytes haven't changed.&lt;/p&gt;

&lt;p&gt;So a checksum covering only &lt;code&gt;key + value&lt;/code&gt; could still pass.&lt;/p&gt;

&lt;p&gt;But the parser would now interpret the same bytes using different boundaries.&lt;/p&gt;

&lt;p&gt;That's a nasty failure mode because the record can look structurally valid while being interpreted incorrectly.&lt;/p&gt;

&lt;p&gt;The fix was to include the length fields themselves in the checksum input:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;key_len
val_len
op
seq
key
value
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anvil has dedicated regression tests for exactly this class of corruption, including a case where the corrupted lengths still add up to the correct total body size.&lt;/p&gt;

&lt;p&gt;That's the kind of bug that is easy to miss if the storage format is treated as "just some bytes."&lt;/p&gt;




&lt;h1&gt;
  
  
  Then I actually killed it
&lt;/h1&gt;

&lt;p&gt;A crash-safe claim is easy to write in a README.&lt;/p&gt;

&lt;p&gt;Testing it is harder.&lt;/p&gt;

&lt;p&gt;So Anvil has a real crash harness that starts a writer, lets it perform writes, and then kills the process with &lt;code&gt;SIGKILL&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then a new process reopens the store.&lt;/p&gt;

&lt;p&gt;The question isn't:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Did Python throw an exception?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"After the process was brutally terminated, can the storage engine still recover every acknowledged write without returning garbage?"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The write-path crash harness passed &lt;strong&gt;8/8 real &lt;code&gt;SIGKILL&lt;/code&gt; iterations&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Each run killed the process after a different number of acknowledged writes, and the recovered store remained usable with the acknowledged data intact. &lt;/p&gt;

&lt;p&gt;That test changed how I think about persistence.&lt;/p&gt;

&lt;p&gt;A successful &lt;code&gt;write()&lt;/code&gt; call is not the end of the story.&lt;/p&gt;

&lt;p&gt;The real question is what survives &lt;strong&gt;after the process is gone&lt;/strong&gt;.&lt;/p&gt;




&lt;h1&gt;
  
  
  Not every damaged record means the same thing
&lt;/h1&gt;

&lt;p&gt;One important recovery decision in Anvil is distinguishing between a truncated final write and actual corruption.&lt;/p&gt;

&lt;p&gt;Imagine the process dies here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[valid record]
[valid record]
[valid record]
[half-written record]
               ↑
             crash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a truncated tail.&lt;/p&gt;

&lt;p&gt;The engine can recognize that the final record isn't complete.&lt;/p&gt;

&lt;p&gt;But this is different:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[valid record]
[valid record]
[CORRUPTED RECORD]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a complete record fails its checksum, Anvil doesn't silently guess what happened.&lt;/p&gt;

&lt;p&gt;It surfaces corruption.&lt;/p&gt;

&lt;p&gt;That's deliberate.&lt;/p&gt;

&lt;p&gt;A storage engine should be conservative about what it considers trustworthy.&lt;/p&gt;




&lt;h1&gt;
  
  
  Compaction: the crash problem doesn't disappear
&lt;/h1&gt;

&lt;p&gt;An append-only log eventually contains obsolete records.&lt;/p&gt;

&lt;p&gt;If I write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PUT counter → 1
PUT counter → 2
PUT counter → 3
PUT counter → 4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;only the latest value is needed for normal reads.&lt;/p&gt;

&lt;p&gt;So Anvil has compaction.&lt;/p&gt;

&lt;p&gt;But compaction introduces another dangerous moment:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if the process dies while creating the new compacted file?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I didn't want a half-written compacted file to replace the original.&lt;/p&gt;

&lt;p&gt;So the process is roughly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;old log
   │
   ▼
write temporary compacted file
   │
   ▼
fsync temporary file
   │
   ▼
atomic os.replace()
   │
   ▼
fsync directory where supported
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The original log remains untouched until the replacement is ready.&lt;/p&gt;

&lt;p&gt;And this isn't just described in the documentation.&lt;/p&gt;

&lt;p&gt;Anvil has a separate compaction crash harness.&lt;/p&gt;

&lt;p&gt;The documented test ran &lt;strong&gt;8/8 real &lt;code&gt;SIGKILL&lt;/code&gt; iterations successfully&lt;/strong&gt;, preserving all 4,000 pre-compaction key/value pairs and allowing compaction to be retried afterwards. &lt;/p&gt;




&lt;h1&gt;
  
  
  Where Python's standard library actually made me suffer
&lt;/h1&gt;

&lt;p&gt;"Zero dependencies" sounds easy if you only look at &lt;code&gt;requirements.txt&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It's much less easy when you start implementing the missing pieces yourself.&lt;/p&gt;

&lt;p&gt;Here's what Anvil uses:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;What Anvil uses&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Binary record format&lt;/td&gt;
&lt;td&gt;&lt;code&gt;struct&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Integrity checking&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zlib.crc32&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File I/O / durability&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;os&lt;/code&gt;, &lt;code&gt;io&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File locking&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;fcntl&lt;/code&gt; / &lt;code&gt;msvcrt&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLI&lt;/td&gt;
&lt;td&gt;&lt;code&gt;argparse&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Testing&lt;/td&gt;
&lt;td&gt;&lt;code&gt;unittest&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Benchmarking&lt;/td&gt;
&lt;td&gt;&lt;code&gt;time.perf_counter&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deterministic test data&lt;/td&gt;
&lt;td&gt;&lt;code&gt;random.Random&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build artifact&lt;/td&gt;
&lt;td&gt;standard-library &lt;code&gt;zipfile&lt;/code&gt; tooling&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The interesting part isn't the module list.&lt;/p&gt;

&lt;p&gt;It's what those modules forced me to implement myself.&lt;/p&gt;

&lt;p&gt;There was no storage library handling the record layout.&lt;/p&gt;

&lt;p&gt;No external locking library handling writers.&lt;/p&gt;

&lt;p&gt;No serialization framework deciding how objects should be persisted.&lt;/p&gt;

&lt;p&gt;No testing framework giving me the crash semantics.&lt;/p&gt;

&lt;p&gt;The standard library gave me primitives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I had to build the policy around them.&lt;/strong&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  I deliberately didn't use &lt;code&gt;sqlite3&lt;/code&gt;
&lt;/h1&gt;

&lt;p&gt;This deserves its own section because &lt;code&gt;sqlite3&lt;/code&gt; is already in Python's standard library.&lt;/p&gt;

&lt;p&gt;Technically, I could have used it.&lt;/p&gt;

&lt;p&gt;But then the project would mostly be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Anvil
  ↓
sqlite3
  ↓
disk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That would defeat the interesting part of Track D.&lt;/p&gt;

&lt;p&gt;Anvil instead owns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the binary format&lt;/li&gt;
&lt;li&gt;the log&lt;/li&gt;
&lt;li&gt;the index&lt;/li&gt;
&lt;li&gt;recovery&lt;/li&gt;
&lt;li&gt;checksums&lt;/li&gt;
&lt;li&gt;locking&lt;/li&gt;
&lt;li&gt;compaction&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The point wasn't to prove that SQLite is bad.&lt;/p&gt;

&lt;p&gt;It's the opposite.&lt;/p&gt;

&lt;p&gt;SQLite is extremely mature.&lt;/p&gt;

&lt;p&gt;The point was to understand what sits underneath a persistent key-value abstraction when you &lt;strong&gt;don't delegate that layer&lt;/strong&gt;.&lt;/p&gt;




&lt;h1&gt;
  
  
  The number I wasn't going to hide
&lt;/h1&gt;

&lt;p&gt;Here's the uncomfortable part.&lt;/p&gt;

&lt;p&gt;Anvil's benchmark isn't trying to win a raw throughput contest.&lt;/p&gt;

&lt;p&gt;With:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;5,000 writes&lt;/li&gt;
&lt;li&gt;5,000 reads&lt;/li&gt;
&lt;li&gt;100-byte values&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;the benchmark reported:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Write time&lt;/td&gt;
&lt;td&gt;0.7247 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write throughput&lt;/td&gt;
&lt;td&gt;6,899.5 ops/sec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read time&lt;/td&gt;
&lt;td&gt;0.0250 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read throughput&lt;/td&gt;
&lt;td&gt;200,015.8 ops/sec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database size&lt;/td&gt;
&lt;td&gt;695,000 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are measurements from the benchmark shipped with the repository, not a theoretical estimate. &lt;/p&gt;

&lt;p&gt;And yes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;the writes are much slower than a memory-speed append.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's intentional.&lt;/p&gt;

&lt;p&gt;Anvil calls &lt;code&gt;fsync&lt;/code&gt; after every write because durability is part of the design.&lt;/p&gt;

&lt;p&gt;So the write path is paying for the storage guarantee:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;append
  ↓
flush
  ↓
fsync
  ↓
acknowledge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result is that write performance is bounded heavily by storage latency.&lt;/p&gt;

&lt;p&gt;I could make the benchmark number look much better by weakening the durability policy.&lt;/p&gt;

&lt;p&gt;I chose not to.&lt;/p&gt;

&lt;p&gt;For this project, &lt;strong&gt;a slower honest write is more useful than a fast write with an unclear durability guarantee.&lt;/strong&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  So is Anvil faster than diskcache?
&lt;/h1&gt;

&lt;p&gt;No.&lt;/p&gt;

&lt;p&gt;And I'm not going to pretend it is.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;diskcache&lt;/code&gt; is a mature project with a much broader feature set.&lt;/p&gt;

&lt;p&gt;Anvil is targeting a narrower problem:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A small persistent key-value storage layer where the application wants to own the storage implementation and keep the runtime dependency surface at zero.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Anvil gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;put&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;get&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;delete&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scan&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;persistence&lt;/li&gt;
&lt;li&gt;crash recovery&lt;/li&gt;
&lt;li&gt;corruption detection&lt;/li&gt;
&lt;li&gt;compaction&lt;/li&gt;
&lt;li&gt;single-writer protection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It does &lt;strong&gt;not&lt;/strong&gt; try to reproduce every feature of a mature cache library.&lt;/p&gt;

&lt;p&gt;No TTL.&lt;/p&gt;

&lt;p&gt;No eviction policy.&lt;/p&gt;

&lt;p&gt;No memoization framework.&lt;/p&gt;

&lt;p&gt;No distributed replication.&lt;/p&gt;

&lt;p&gt;No multi-key transactions.&lt;/p&gt;

&lt;p&gt;That's an important distinction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Anvil is a focused storage engine, not a replacement for every database or cache.&lt;/strong&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  Making the claims easy to verify
&lt;/h1&gt;

&lt;p&gt;One thing I wanted to avoid was a README full of claims that a judge simply had to trust.&lt;/p&gt;

&lt;p&gt;So Anvil has a verification mindset built into the project.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python scripts/check_deps.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;checks the actual imports used by the source.&lt;/p&gt;

&lt;p&gt;The repository documents the expected result as standard-library-only imports with an empty dependency manifest. &lt;/p&gt;

&lt;p&gt;There is also a dedicated &lt;code&gt;verify&lt;/code&gt; command that can inspect the storage file and report:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;path: user.anvil
file size: 46 byte(s)
valid record count: 2
put records: 2
delete records: 0
live key count: 2
verified up to offset: 46
result: PASS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So verification isn't just:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Trust me, the database is fine."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It's a separate operation.&lt;/p&gt;




&lt;h1&gt;
  
  
  Testing more than the happy path
&lt;/h1&gt;

&lt;p&gt;The test suite doesn't only check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;put → get → success
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It covers things such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;persistence across restart&lt;/li&gt;
&lt;li&gt;delete semantics&lt;/li&gt;
&lt;li&gt;empty values&lt;/li&gt;
&lt;li&gt;large values&lt;/li&gt;
&lt;li&gt;malformed records&lt;/li&gt;
&lt;li&gt;checksum corruption&lt;/li&gt;
&lt;li&gt;corrupted length fields&lt;/li&gt;
&lt;li&gt;truncated final records&lt;/li&gt;
&lt;li&gt;concurrent writer handling&lt;/li&gt;
&lt;li&gt;lock release&lt;/li&gt;
&lt;li&gt;compaction correctness&lt;/li&gt;
&lt;li&gt;crash recovery during compaction&lt;/li&gt;
&lt;li&gt;CLI exit codes&lt;/li&gt;
&lt;li&gt;reproducible-build verification&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The current project documentation records a &lt;strong&gt;100-test &lt;code&gt;unittest&lt;/code&gt; suite&lt;/strong&gt; covering these behaviors. &lt;/p&gt;

&lt;p&gt;The important part isn't the number 100 by itself.&lt;/p&gt;

&lt;p&gt;It's that the tests are aimed at the places where storage engines usually become dangerous:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;partial writes, corruption, concurrency and replacement.&lt;/strong&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  I also wanted the build to be reproducible
&lt;/h1&gt;

&lt;p&gt;The final artifact can be built deterministically.&lt;/p&gt;

&lt;p&gt;The build script fixes things such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;file ordering&lt;/li&gt;
&lt;li&gt;timestamps&lt;/li&gt;
&lt;li&gt;platform flags&lt;/li&gt;
&lt;li&gt;permissions&lt;/li&gt;
&lt;li&gt;compression settings&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two builds from the same source produced the same SHA-256:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fc5b367929a65c9db9e859eda91b491f8c59ef5d1b79e6ccaaf84ab062c7ffd4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;for both artifacts. &lt;/p&gt;

&lt;p&gt;There's also a deliberately stated limitation:&lt;/p&gt;

&lt;p&gt;this proves byte-identical output for the tested build environment/toolchain; it isn't a claim that arbitrary future Python or &lt;code&gt;zlib&lt;/code&gt; versions will necessarily produce the same compressed bytes. &lt;/p&gt;

&lt;p&gt;Again, the goal is not to make the claim sound bigger.&lt;/p&gt;

&lt;p&gt;It's to make the claim &lt;strong&gt;precise&lt;/strong&gt;.&lt;/p&gt;




&lt;h1&gt;
  
  
  What I learned
&lt;/h1&gt;

&lt;p&gt;When I started, the interesting problem looked like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"How do I implement a key-value store?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;By the end, that wasn't really the problem.&lt;/p&gt;

&lt;p&gt;The interesting questions were:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When is a write durable?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What does a crash leave behind?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which bytes can I trust?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens if a length field is corrupted but the record still has the right total size?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens if compaction dies halfway through?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens if another process tries to write at the same time?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Those questions shaped almost every important part of Anvil.&lt;/p&gt;

&lt;p&gt;And that's probably the biggest lesson I got from building it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Writing data to disk is easy. Knowing when you can trust that data is the real problem.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  What Anvil doesn't try to be
&lt;/h1&gt;

&lt;p&gt;Anvil is intentionally small.&lt;/p&gt;

&lt;p&gt;It is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;embedded&lt;/li&gt;
&lt;li&gt;persistent&lt;/li&gt;
&lt;li&gt;log-structured&lt;/li&gt;
&lt;li&gt;single-writer&lt;/li&gt;
&lt;li&gt;crash-recoverable&lt;/li&gt;
&lt;li&gt;standard-library-only&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is not:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a distributed database&lt;/li&gt;
&lt;li&gt;a full SQL database&lt;/li&gt;
&lt;li&gt;a multi-writer transactional engine&lt;/li&gt;
&lt;li&gt;a feature-complete cache&lt;/li&gt;
&lt;li&gt;a cryptographically authenticated storage system&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CRC32 gives integrity detection.&lt;/p&gt;

&lt;p&gt;It does &lt;strong&gt;not&lt;/strong&gt; provide cryptographic authenticity.&lt;/p&gt;

&lt;p&gt;Those boundaries are intentional.&lt;/p&gt;

&lt;p&gt;A project becomes more useful when its guarantees are clear.&lt;/p&gt;




&lt;h1&gt;
  
  
  Final Thoughts
&lt;/h1&gt;

&lt;p&gt;I started Anvil because the Zero Dependency constraint made me ask a slightly uncomfortable question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What if I couldn't rent the storage layer from someone else?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The answer turned out to be much more than a dictionary backed by a file.&lt;/p&gt;

&lt;p&gt;It meant designing a record format.&lt;/p&gt;

&lt;p&gt;It meant deciding what a checksum actually protects.&lt;/p&gt;

&lt;p&gt;It meant rebuilding the index after a crash.&lt;/p&gt;

&lt;p&gt;It meant distinguishing a truncated write from real corruption.&lt;/p&gt;

&lt;p&gt;It meant making compaction atomic.&lt;/p&gt;

&lt;p&gt;It meant testing the system by actually killing it.&lt;/p&gt;

&lt;p&gt;And it meant accepting a slower write path because durability mattered more than a pretty benchmark number.&lt;/p&gt;

&lt;p&gt;Anvil is still a small storage engine.&lt;/p&gt;

&lt;p&gt;But building it changed the way I think about persistence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The hard part isn't storing the value.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The hard part is knowing when you can trust it.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Built for Hackathon Raptors' Zero Dependency Hackathon 2026 — Track D: Data &amp;amp; Storage.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository:&lt;/strong&gt; &lt;a href="https://github.com/arfaaa243/anvil-db" rel="noopener noreferrer"&gt;Anvil on GitHub&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Repository:&lt;/strong&gt; &lt;a href="https://github.com/arfaaa243/anvil-db" rel="noopener noreferrer"&gt;https://github.com/arfaaa243/anvil-db&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zero runtime dependencies.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Real crash testing.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Own storage format.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;No &lt;code&gt;sqlite3&lt;/code&gt; underneath.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>python</category>
      <category>opensource</category>
      <category>database</category>
    </item>
  </channel>
</rss>
