<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Vrutik Halani]]></title><description><![CDATA[Vrutik Halani]]></description><link>https://vrutik-halani.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Thu, 10 Sep 2026 03:11:31 GMT</lastBuildDate><atom:link href="https://vrutik-halani.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[VrootKV Build Log #1: Abstracting the Filesystem in C++]]></title><description><![CDATA[Hey everyone!
I'm embarking on a pretty ambitious project, and I've decided to build it in public and share the journey with all of you. I'm building VrootKV: a high-performance, transactional key-value storage engine from scratch in modern C++.
This...]]></description><link>https://vrutik-halani.hashnode.dev/vrootkv-build-log-1-abstracting-the-filesystem-in-c</link><guid isPermaLink="true">https://vrutik-halani.hashnode.dev/vrootkv-build-log-1-abstracting-the-filesystem-in-c</guid><category><![CDATA[vrootkv]]></category><category><![CDATA[Databases]]></category><category><![CDATA[key-value store]]></category><category><![CDATA[cpp]]></category><category><![CDATA[System Design]]></category><category><![CDATA[Build In Public]]></category><category><![CDATA[Programming Blogs]]></category><dc:creator><![CDATA[Vrutik Halani]]></dc:creator><pubDate>Wed, 10 Sep 2025 17:38:56 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1757525831210/e0b7168c-7676-4860-bc11-1fc2c57dd275.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hey everyone!</p>
<p>I'm embarking on a pretty ambitious project, and I've decided to build it in public and share the journey with all of you. I'm building <strong>VrootKV</strong>: a high-performance, transactional key-value storage engine from scratch in modern C++.</p>
<p>This blog series will be my build log, documenting the entire process from the lowest-level file I/O all the way up to complex multi-version concurrency control (MVCC). I'll be sharing my design decisions, the code, the tests, and the lessons I learn along the way.</p>
<p>So, where do you start when building a database? Before we can write a single byte of data, we need to decide <em>how</em> we're going to talk to the disk. It might be tempting to just sprinkle <code>std::ofstream</code> or <code>fopen</code> calls throughout the codebase, but for a serious system like this, that's a recipe for disaster. We need to start with a solid, clean foundation.</p>
<p>That foundation is the I/O Abstraction Layer.</p>
<h3 id="heading-why-bother-abstracting-the-filesystem">Why Bother Abstracting the Filesystem?</h3>
<p>At first glance, creating a whole layer just to read and write files might seem like over-engineering. Why not just use the standard library or OS-level file APIs directly? It comes down to three critical principles for systems programming that save you from a world of pain later on :</p>
<ol>
<li><p><strong>Testability:</strong> How do you unit test logic that writes to disk? You don't want your tests to be constantly creating and deleting real files. That makes them slow, flaky, and dependent on the host machine's state. By abstracting file operations behind an interface, we can easily create a "mock" file manager in our tests that simulates disk behavior entirely in memory. This makes our unit tests fast, reliable, and perfectly isolated.</p>
</li>
<li><p><strong>Portability:</strong> The core logic of a storage engine shouldn't be tied to a specific operating system. While I'm developing on macOS, I want VrootKV to compile and run on Linux and Windows without changing a single line of the database's core logic. An abstraction layer allows us to hide the platform-specific details (<code>open</code>/<code>write</code> on POSIX vs. <code>CreateFileW</code>/<code>WriteFile</code> on Windows) behind a common, clean API.</p>
</li>
<li><p><strong>Clarity and Intent:</strong> A well-defined interface clearly communicates the exact requirements the storage engine has for the filesystem. It forms a contract. We don't just need to "write bytes"; we need to guarantee those bytes are durably persisted. This is a crucial distinction for a database, and our interface should make that intent explicit with methods like <code>Sync()</code>.</p>
</li>
</ol>
<h3 id="heading-designing-the-io-interfaces">Designing the I/O Interfaces</h3>
<p>With those goals in mind, I created a set of C++ interfaces that define the contract for all file system interactions in VrootKV. The entire public API is defined in <code>file_manager.h</code>. Let's walk through it piece by piece.</p>
<p>First, we need a contract for a file we can write to.</p>
<pre><code class="lang-cpp"><span class="hljs-comment">// From: include/VrootKV/io/file_manager.h</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">IWritableFile</span> {</span>
<span class="hljs-keyword">public</span>:
    <span class="hljs-keyword">virtual</span> ~IWritableFile() = <span class="hljs-keyword">default</span>;

    <span class="hljs-function"><span class="hljs-keyword">virtual</span> <span class="hljs-keyword">bool</span> <span class="hljs-title">Write</span><span class="hljs-params">(<span class="hljs-built_in">std</span>::string_view data)</span> </span>= <span class="hljs-number">0</span>;
    <span class="hljs-function"><span class="hljs-keyword">virtual</span> <span class="hljs-keyword">bool</span> <span class="hljs-title">Flush</span><span class="hljs-params">()</span> </span>= <span class="hljs-number">0</span>;
    <span class="hljs-function"><span class="hljs-keyword">virtual</span> <span class="hljs-keyword">bool</span> <span class="hljs-title">Sync</span><span class="hljs-params">()</span> </span>= <span class="hljs-number">0</span>;
    <span class="hljs-function"><span class="hljs-keyword">virtual</span> <span class="hljs-keyword">bool</span> <span class="hljs-title">Close</span><span class="hljs-params">()</span> </span>= <span class="hljs-number">0</span>;
};
</code></pre>
<p>The most important methods here are <code>Flush()</code> and <code>Sync()</code>. They look similar but represent very different guarantees. <code>Flush()</code> typically just tells the operating system to move data from the application's buffer to the OS's buffer. <code>Sync()</code>, on the other hand, is a much stronger promise. It requests that the OS physically write the data to the storage device itself. For a database's Write-Ahead Log (WAL), <code>Sync()</code> is the operation that actually provides durability.</p>
<p>Next, the interface for reading files is simpler, as it's primarily concerned with getting sequential data.</p>
<pre><code class="lang-cpp"><span class="hljs-comment">// From: include/VrootKV/io/file_manager.h</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">IReadableFile</span> {</span>
<span class="hljs-keyword">public</span>:
    <span class="hljs-keyword">virtual</span> ~IReadableFile() = <span class="hljs-keyword">default</span>;
    <span class="hljs-function"><span class="hljs-keyword">virtual</span> <span class="hljs-keyword">size_t</span> <span class="hljs-title">Read</span><span class="hljs-params">(<span class="hljs-keyword">size_t</span> n, <span class="hljs-built_in">std</span>::<span class="hljs-built_in">string</span>* result)</span> </span>= <span class="hljs-number">0</span>;
    <span class="hljs-function"><span class="hljs-keyword">virtual</span> <span class="hljs-keyword">bool</span> <span class="hljs-title">Close</span><span class="hljs-params">()</span> </span>= <span class="hljs-number">0</span>;
};
</code></pre>
<p>Finally, the <code>IFileManager</code> interface ties everything together. It acts as a factory for creating file objects and also handles path-level operations like checking for existence, deleting, and renaming.</p>
<pre><code class="lang-cpp"><span class="hljs-comment">// From: include/VrootKV/io/file_manager.h</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">IFileManager</span> {</span>
<span class="hljs-keyword">public</span>:
    <span class="hljs-keyword">virtual</span> ~IFileManager() = <span class="hljs-keyword">default</span>;

    <span class="hljs-function"><span class="hljs-keyword">virtual</span> <span class="hljs-keyword">bool</span> <span class="hljs-title">NewWritableFile</span><span class="hljs-params">(<span class="hljs-keyword">const</span> <span class="hljs-built_in">std</span>::<span class="hljs-built_in">string</span>&amp; fname, <span class="hljs-built_in">std</span>::<span class="hljs-built_in">unique_ptr</span>&lt;IWritableFile&gt;&amp; result)</span> </span>= <span class="hljs-number">0</span>;
    <span class="hljs-function"><span class="hljs-keyword">virtual</span> <span class="hljs-keyword">bool</span> <span class="hljs-title">NewReadableFile</span><span class="hljs-params">(<span class="hljs-keyword">const</span> <span class="hljs-built_in">std</span>::<span class="hljs-built_in">string</span>&amp; fname, <span class="hljs-built_in">std</span>::<span class="hljs-built_in">unique_ptr</span>&lt;IReadableFile&gt;&amp; result)</span> </span>= <span class="hljs-number">0</span>;
    <span class="hljs-function"><span class="hljs-keyword">virtual</span> <span class="hljs-keyword">bool</span> <span class="hljs-title">FileExists</span><span class="hljs-params">(<span class="hljs-keyword">const</span> <span class="hljs-built_in">std</span>::<span class="hljs-built_in">string</span>&amp; fname)</span> <span class="hljs-keyword">const</span> </span>= <span class="hljs-number">0</span>;
    <span class="hljs-function"><span class="hljs-keyword">virtual</span> <span class="hljs-keyword">bool</span> <span class="hljs-title">DeleteFile</span><span class="hljs-params">(<span class="hljs-keyword">const</span> <span class="hljs-built_in">std</span>::<span class="hljs-built_in">string</span>&amp; fname)</span> </span>= <span class="hljs-number">0</span>;
    <span class="hljs-function"><span class="hljs-keyword">virtual</span> <span class="hljs-keyword">bool</span> <span class="hljs-title">RenameFile</span><span class="hljs-params">(<span class="hljs-keyword">const</span> <span class="hljs-built_in">std</span>::<span class="hljs-built_in">string</span>&amp; src, <span class="hljs-keyword">const</span> <span class="hljs-built_in">std</span>::<span class="hljs-built_in">string</span>&amp; target)</span> </span>= <span class="hljs-number">0</span>;
};
</code></pre>
<p>Notice the use of <code>std::unique_ptr</code>. This is a modern C++ feature that ensures the file objects are automatically cleaned up when they go out of scope, preventing resource leaks without manual memory management.</p>
<h3 id="heading-the-implementation-hiding-the-messy-details">The Implementation: Hiding the Messy Details</h3>
<p>With the interfaces defined, the implementation in <code>file_manager.cpp</code> can now hide all the platform-specific ugliness. I'm using C++17 <code>std::filesystem</code> for path operations (like <code>exists</code>, <code>rename</code>) as it's now standard and cross-platform. For the actual file I/O, I use preprocessor directives (<code>#ifdef _WIN32</code>) to switch between POSIX and Win32 APIs.</p>
<p>This is where the abstraction pays off. For example, look at the POSIX implementation of <code>Write()</code>. It's not a simple one-liner. It has to be in a loop to handle "partial writes" (where the OS doesn't write all the data you requested at once) and it has to correctly handle being interrupted by a system signal (<code>EINTR</code>).</p>
<pre><code class="lang-cpp"><span class="hljs-comment">// Snippet from: src/io/file_manager.cpp</span>

<span class="hljs-function"><span class="hljs-keyword">bool</span> <span class="hljs-title">PosixWritableFile::Write</span><span class="hljs-params">(<span class="hljs-built_in">std</span>::string_view data)</span> </span>{
    <span class="hljs-keyword">if</span> (fd_ == <span class="hljs-number">-1</span>) <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;

    <span class="hljs-keyword">const</span> <span class="hljs-keyword">char</span>* p = data.data();
    <span class="hljs-keyword">size_t</span> remaining = data.size();

    <span class="hljs-keyword">while</span> (remaining &gt; <span class="hljs-number">0</span>) {
        <span class="hljs-keyword">ssize_t</span> n = ::write(fd_, p, remaining);
        <span class="hljs-keyword">if</span> (n &lt; <span class="hljs-number">0</span>) {
            <span class="hljs-keyword">if</span> (errno == EINTR) <span class="hljs-keyword">continue</span>; <span class="hljs-comment">// Retry write</span>
            <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;
        }
        <span class="hljs-comment">//... handle partial write...</span>
        p += <span class="hljs-keyword">static_cast</span>&lt;<span class="hljs-keyword">size_t</span>&gt;(n);
        remaining -= <span class="hljs-keyword">static_cast</span>&lt;<span class="hljs-keyword">size_t</span>&gt;(n);
    }
    <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
}
</code></pre>
<p>This is exactly the kind of complex, error-prone logic we want to write once, test thoroughly, and then hide behind our clean <code>IWritableFile</code> interface. The rest of the storage engine will never have to worry about these details.</p>
<p>You can see the full cross-platform implementation in the source file on GitHub: <a target="_blank" href="https://www.google.com/search?q=%5Bhttps://github.com/dreamvrutik/VrootKV/blob/main/src/io/file_manager.cpp%5D\(https://github.com/dreamvrutik/VrootKV/blob/main/src/io/file_manager.cpp\)&amp;authuser=1"><code>src/io/file_manager.cpp</code></a>.</p>
<h3 id="heading-verification-proving-it-works">Verification: Proving It Works</h3>
<p>An abstraction layer is useless if it's not reliable. Rigorous unit testing is non-negotiable for a foundational component like this. Using the Google Test framework, I set up a test fixture that creates a unique temporary directory for each test case, ensuring a clean, isolated environment every time.</p>
<p>The tests cover every aspect of the interface. A key test is <code>WriteAndSyncFile</code>, which validates that data written through the interface is actually persisted durably to disk.</p>
<pre><code class="lang-cpp"><span class="hljs-comment">// Snippet from: tests/io/test_file_manager.cpp</span>

TEST_F(FileManagerTest, WriteAndSyncFile) {
    <span class="hljs-keyword">const</span> <span class="hljs-built_in">std</span>::<span class="hljs-built_in">string</span> filename = TestPath(<span class="hljs-string">"test_write.txt"</span>);
    <span class="hljs-built_in">std</span>::<span class="hljs-built_in">unique_ptr</span>&lt;IWritableFile&gt; writable_file;

    <span class="hljs-comment">// Create/truncate the file for writing.</span>
    ASSERT_TRUE(file_manager_-&gt;NewWritableFile(filename, writable_file));

    <span class="hljs-keyword">const</span> <span class="hljs-built_in">std</span>::<span class="hljs-built_in">string</span> data1 = <span class="hljs-string">"Hello, "</span>;
    <span class="hljs-keyword">const</span> <span class="hljs-built_in">std</span>::<span class="hljs-built_in">string</span> data2 = <span class="hljs-string">"World!"</span>;

    EXPECT_TRUE(writable_file-&gt;Write(data1));
    EXPECT_TRUE(writable_file-&gt;Write(data2));

    <span class="hljs-comment">// Sync ensures durability beyond the OS page cache.</span>
    EXPECT_TRUE(writable_file-&gt;Sync());
    EXPECT_TRUE(writable_file-&gt;Close());

    <span class="hljs-comment">// Verify the file content matches concatenated writes.</span>
    <span class="hljs-function"><span class="hljs-built_in">std</span>::ifstream <span class="hljs-title">ifs</span><span class="hljs-params">(filename)</span></span>;
    <span class="hljs-built_in">std</span>::<span class="hljs-built_in">stringstream</span> buffer;
    buffer &lt;&lt; ifs.rdbuf();
    EXPECT_EQ(buffer.str(), data1 + data2);
}
</code></pre>
<p>This test writes some data, calls <code>Sync()</code>, and then reads the file back using standard library tools to verify the contents are exactly as expected. This confirms our core contract of durability.</p>
<p>The full test suite covers many other cases, including chunked reads, error conditions, and file management operations. You can explore it here: <a target="_blank" href="https://www.google.com/search?q=%5Bhttps://github.com/dreamvrutik/VrootKV/blob/main/tests/io/test_file_manager.cpp%5D\(https://github.com/dreamvrutik/VrootKV/blob/main/tests/io/test_file_manager.cpp\)&amp;authuser=1"><code>tests/io/test_file_manager.cpp</code></a>.</p>
<h3 id="heading-conclusion-and-whats-next">Conclusion and What's Next</h3>
<p>And there we have it! The very first component of VrootKV is complete. We now have a robust, cross-platform, and thoroughly tested I/O abstraction layer. This solid foundation will make everything we build on top of it cleaner and more reliable.</p>
<p>In the next post, we'll use this <code>IFileManager</code> to start defining and implementing the on-disk formats for our Write-Ahead Log (WAL) and SSTables. That's where things start to get really interesting.</p>
<p>Thanks for reading! You can find the full, up-to-date source code for the project on my GitHub. Feel free to star the project, open issues, or leave any questions or feedback in the comments below!</p>
<p><strong>GitHub Repo:</strong> <a target="_blank" href="https://github.com/dreamvrutik/VrootKV">https://github.com/dreamvrutik/VrootKV</a></p>
<p><strong>LinkedIn:</strong> <a target="_blank" href="https://www.linkedin.com/in/vrutik-halani"><strong>https://www.linkedin.com/in/vrutik-halani</strong></a></p>
<p>#cpp #database #systemdesign #buildinpublic #vrootkv #programming #backend #datastructures</p>
]]></content:encoded></item></channel></rss>