<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Inside DBs — Ricardo Jiménez-Peris</title>
    <link>https://ricardojimenezperis.com/</link>
    <description>Notes on how databases actually work, one system at a time. By Ricardo Jiménez-Peris.</description>
    <generator>Hugo</generator>
    <language>en</language>
    <lastBuildDate>Sat, 12 Sep 2026 10:00:00 +0200</lastBuildDate>
    <atom:link href="https://ricardojimenezperis.com/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>FoundationDB: the architecture, seen through a transaction</title>
      <link>https://ricardojimenezperis.com/systems/foundationdb-architecture/</link>
      <guid isPermaLink="true">https://ricardojimenezperis.com/systems/foundationdb-architecture/</guid>
      <pubDate>Sat, 12 Sep 2026 10:00:00 +0200</pubDate>
      <dc:creator>Ricardo Jiménez-Peris</dc:creator>
      <category>foundationdb</category>
      <category>architecture</category>
      <category>transactions</category>
      <category>mvcc</category>
      <description>FoundationDB&#39;s components introduced by following one transaction through them — from read version to readable.</description>
      <content:encoded><![CDATA[<p>FoundationDB is a transactional database with an unusual shape: its architecture separates transaction processing from storage, allowing the two to scale independently. There is another database, LeanXcale, that took the same route. The two share enough that they look like twins who agreed on the overall approach, were then separated, and made slightly different decisions because they were solving different problems. That story is for another post. Here the goal is to introduce FoundationDB&rsquo;s components by following a transaction through them, from the moment it starts to the moment its writes become readable. The next post will explain FoundationDB&rsquo;s five-second read-version window and how it constrains transactions.</p>
<p>The first architectural point is that FoundationDB is a transactional key-value store. There is no SQL engine inside it. A SQL interface exists — the Record Layer&rsquo;s relational layer — but it is a library that runs inside the application, not a server-side component.</p>
<p>The second distinctive point is that there is no single server side that performs operations on the client&rsquo;s behalf. The client library talks to the storage servers directly, and to two components that orchestrate transactions: GRV proxies, which provide read versions, and commit proxies, which commit transactions.</p>
<figure><a href="https://ricardojimenezperis.com/systems/foundationdb-architecture/transaction-lifecycle.png">
    <img loading="lazy" src="https://ricardojimenezperis.com/systems/foundationdb-architecture/transaction-lifecycle.png"
         alt="FoundationDB&#39;s components and the messages exchanged during a transaction"/> </a>
</figure>

<p class="figure-caption"><em>Figure 1. FoundationDB's components and the messages exchanged during a transaction, from read version to readable.</em></p>
<p>The figure names the actual requests the components exchange. You don&rsquo;t need any of them to follow what comes next — they are there for a second reading, and for the next post.</p>
<h2 id="versions">Versions</h2>
<p>FoundationDB retains multiple versions of key-value data so that transactions can read consistent snapshots. Versions are tagged with monotonically increasing commit versions that fix the order in which transactions serialize. The component that hands out those versions is the master, and there is one active master at a time.</p>
<p>To keep client applications from overwhelming a singleton, they never talk to the master directly — the proxies do it for them.</p>
<p>This gives each transaction a snapshot of the database as of its start. A client starting a transaction asks a GRV proxy for a read version. The proxy batches these requests and fetches one version from the master for the whole batch, which is what keeps traffic to the singleton bounded.</p>
<h2 id="execution">Execution</h2>
<p>Reads go straight to the storage servers, carrying the read version. A storage server returns, for each key, the value with the highest commit version less than or equal to that read version.</p>
<p><strong>Example.</strong> Suppose key <code>k5</code> has three versions:</p>
<table>
	<thead>
			<tr>
					<th>Version</th>
					<th>Commit version</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>k5</code> v1</td>
					<td>10</td>
			</tr>
			<tr>
					<td><code>k5</code> v2</td>
					<td>15</td>
			</tr>
			<tr>
					<td><code>k5</code> v3</td>
					<td>20</td>
			</tr>
	</tbody>
</table>
<p>A transaction with read version 16 reading <code>k5</code> gets <strong>v2</strong>: 15 is the highest commit version for <code>k5</code> that is less than or equal to 16. Version 3 committed at 20, after the transaction&rsquo;s snapshot, so the transaction cannot see it.</p>
<p>Writes are different: they are buffered in the client. To give read-your-own-writes semantics, reads have to be merged with whatever is sitting in that write buffer.</p>
<p><strong>Example.</strong> Suppose storage holds <code>k0</code>, <code>k1</code>, <code>k8</code> and <code>k9</code>, and the transaction writes <code>k5</code>.</p>
<p>A point read of <code>k5</code> returns the value the transaction just wrote. <code>k5</code> does not exist in storage at all, so without the merge the read would come back empty.</p>
<p>A range read over <code>[k1, k9)</code> returns <code>k1</code>, <code>k5</code> and <code>k8</code>. Two of them, <code>k1</code> and <code>k8</code>, come from the storage servers; <code>k5</code> comes from the write buffer and has to be spliced into the result in key order. <code>k0</code> falls before the start of the range and <code>k9</code> is its exclusive end.</p>
<h2 id="commit">Commit</h2>
<p>A read-only transaction is finished at this point — there is nothing left to do.</p>
<p>If the transaction wrote, the client asks a commit proxy to do the rest.</p>
<p>The commit proxy first obtains a commit version from the master. It then sends the transaction&rsquo;s conflict ranges to the resolvers, which check whether any write committed after the transaction&rsquo;s read version intersects its read conflict ranges. That check is what enforces isolation. The guarantee is strict serializability: read versions come from a single master and reflect everything committed before them, so a transaction that begins after another has committed is guaranteed to observe it.</p>
<p>If no conflict is found, the write set — mutations, in FoundationDB&rsquo;s vocabulary — is made durable in the transaction logs. That is the point of no return: once it completes, the transaction is committed.</p>
<p>Storage servers continuously pull mutations from the transaction logs and apply them asynchronously. Transactions whose read version is at or beyond a mutation&rsquo;s commit version observe that change; if a storage server has not yet reached the requested read version, the read waits until it catches up.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
