<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>James Long</title>
  <link href="https://archive.jlongster.com/atom.xml" rel="self" />
  <link href="https://archive.jlongster.com/" />
  <updated>2020-05-26T00:00:00Z</updated>
  <id>https://archive.jlongster.com/</id>
  <author><name>James Long</name></author>

  
  <entry>
    <title>How one word in PostgreSQL unlocked a 9x performance improvement</title>
    <link href="https://archive.jlongster.com/how-one-word-postgresql-performance" />
    <published>2020-05-26T00:00:00Z</published>
    <updated>2020-05-26T00:00:00Z</updated>
    <id>https://archive.jlongster.com/how-one-word-postgresql-performance</id>
    <summary type="html"><![CDATA[<p id="p1">At the very heart of <a href="https://actualbudget.com/">Actual</a> is a custom syncing engine. Recently I implemented full end-to-end encryption (not released yet) and it inspired me to audit the performance of the whole process. In the future I'll blog more about using CRDTs for syncing, but for now I'd like to talk about a PostgreSQL feature that enabled a 9-10x performance improvement.</p>

<p id="p2">Actual is completely a local app and syncing happens in the background (using <a href="https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type">CRDTs</a>). This means the server is very simple and all it has to do is store and fetch "messages" for clients. The entire code for handling syncing is only ~200 lines of JavaScript.</p>

<p id="p3">We need to handle <em>a lot</em> of messages to keep syncing fast. In fact, while working on this something strange happened: a new user generated 169,000 messages on one day. This is an outlier by a <em>huge</em> margin. For example, importing 1000 transactions into the system would generate about 6000 messages and, while reasonable, is still more than the average number of message per day per user. I believe they did this by using the API trying to bulk import a lot of data and we have <a href="https://actualbudget.com/docs/developers/using-the-API/#writing-data-importers">different APIs</a> for that. Still, I thought, what if I made <strong>169,000</strong> my benchmark?</p>

<p id="p4">I tried pumping 169,000 messages through the system and broke the server. The request timed out and the server was still crunching through messages making everything else slow. I knew what the problem was instantly.</p>]]></summary>
    <content type="html"><![CDATA[<p id="p6">At the very heart of <a href="https://actualbudget.com/">Actual</a> is a custom syncing engine. Recently I implemented full end-to-end encryption (not released yet) and it inspired me to audit the performance of the whole process. In the future I'll blog more about using CRDTs for syncing, but for now I'd like to talk about a PostgreSQL feature that enabled a 9-10x performance improvement.</p>

<p id="p7">Actual is completely a local app and syncing happens in the background (using <a href="https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type">CRDTs</a>). This means the server is very simple and all it has to do is store and fetch "messages" for clients. The entire code for handling syncing is only ~200 lines of JavaScript.</p>

<p id="p8">We need to handle <em>a lot</em> of messages to keep syncing fast. In fact, while working on this something strange happened: a new user generated 169,000 messages on one day. This is an outlier by a <em>huge</em> margin. For example, importing 1000 transactions into the system would generate about 6000 messages and, while reasonable, is still more than the average number of message per day per user. I believe they did this by using the API trying to bulk import a lot of data and we have <a href="https://actualbudget.com/docs/developers/using-the-API/#writing-data-importers">different APIs</a> for that. Still, I thought, what if I made <strong>169,000</strong> my benchmark?</p>

<p id="p9">I tried pumping 169,000 messages through the system and broke the server. The request timed out and the server was still crunching through messages making everything else slow. I knew what the problem was instantly.</p>

<p id="p10">Messages are stored in PostgreSQL and the table looks like this:</p>

<pre><code class="sql hljs">CREATE TABLE messages_binary
  (timestamp TEXT,
   group_id TEXT,
   is_encrypted BOOLEAN,
   content bytea,
   PRIMARY KEY(timestamp, group_id));
</code></pre>

<p id="p11">It stores small binary blobs marked with a timestamp and a "sync group" they belong to.</p>

<p id="p12">The server was choking trying to insert so many rows. Unfortunately, we can't simply execute one query with a bunch of <code>INSERT</code> statements when adding messages. Our CRDTs have a few constraints:</p>

<ol>
<li id="li1">Message can't ever be duplicated (identified by <code>timestamp</code>)</li>
<li id="li2">We need to update a <a href="https://en.wikipedia.org/wiki/Merkle_tree">merkle trie</a> depending on whether or not the message was added</li>
</ol>

<p id="p13">Solving <code>#1</code> is easy. Because we made <code>timestamp</code> the primary key, we can do <code>INSERT INTO messages_binary (...) VALUES (...) ON CONFLICT DO NOTHING</code>. The <code>ON CONFLICT</code> clause tells it to do nothing when there's a conflict, and duplicates conflict on primary key.</p>

<p id="p14">A much bigger problem is <code>#2</code>. We need the <em>result</em> of the insert to know if a row was inserted or not. If it was inserted, we need to also update our merkle trie like this:</p>

<pre><code class="javascript hljs"><span class="hljs-keyword">if</span>(inserted) {
  trie = merkle.insert(trie, Timestamp.parse(msg.timestamp));
}
</code></pre>

<p id="p15">It's extremely important that each timestamp in the system only ever get inserted to the merkle trie once. The trie is responsible for guaranteeing consistency in the system and maintains hashes for the content. If you haven't added each timestamp once and only once, the hashes (and thus verification) are wrong.</p>

<p id="p16">The whole code for updating the database looks like this (using some abstractions over <a href="https://node-postgres.com/">node-postgres</a>):</p>

<pre><code class="javascript hljs"><span class="hljs-keyword">await</span> runQuery(<span class="hljs-string">'BEGIN'</span>);
<span class="hljs-keyword">let</span> trie = <span class="hljs-keyword">await</span> getMerkle(runQuery, groupId);

<span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> message <span class="hljs-keyword">of</span> messages) {
  <span class="hljs-keyword">let</span> timestamp = message.getTimestamp();
  <span class="hljs-keyword">let</span> isEncrypted = message.getIsencrypted();
  <span class="hljs-keyword">let</span> content = message.getContent();

  <span class="hljs-keyword">let</span> { changes } = <span class="hljs-keyword">await</span> runQuery(
    <span class="hljs-string">`INSERT INTO messages_binary (timestamp, group_id, is_encrypted, content)
       VALUES ($1, $2, $3, $4) ON CONFLICT DO NOTHING`</span>,
    [timestamp, groupId, isEncrypted, content]
  );

  <span class="hljs-keyword">if</span> (changes === <span class="hljs-number">1</span>) {
    <span class="hljs-comment">// Update the merkle trie</span>
    trie = merkle.insert(trie, Timestamp.parse(timestamp));
  }
}

<span class="hljs-keyword">await</span> runQuery(
  <span class="hljs-string">`INSERT INTO messages_merkles (group_id, merkle)
     VALUES ($1, $2)
         ON CONFLICT (group_id) DO UPDATE SET merkle = $2`</span>,
  [groupId, <span class="hljs-built_in">JSON</span>.stringify(trie)]
);

<span class="hljs-keyword">await</span> runQuery(<span class="hljs-string">'COMMIT'</span>);
</code></pre>

<p id="p17">This is mostly the real code, the only difference is we also rollback the transaction on failure. It's <em>extremely</em> important that this happens in a transaction and both the messages and merkle trie are updated <a href="https://archive.jlongster.com/wonderful-sound-atomic-commit">atomically</a>. Again, the merkle trie verifies the messages content and they must always be in sync. The user will see sync errors if they are not.</p>

<p id="p18">The problem is immediately clear: we are executing an <code>INSERT</code> query for each message individually. In our extreme case we are trying to execute 169,000 statements. PostgreSQL lives on a different server (but close) and making that many network calls alone is going to kill performance, not to mention PG overhead.</p>

<p id="p19">I knew this was slow, but I didn't realize how slow. Let's test a more reasonable number of messages that actually finishes. <strong>4000 messages takes 6.9s to complete</strong>. This is just profiling the above code, and not taking into account network transfer.</p>

<p id="p20">This is a <em>huge</em> UX issue. While this is processing the user is sitting there watching the "sync" icon spin and spin and spin…</p>

<p id="p21">Back to the drawing board. What we need: </p>

<ul>
<li id="li3">To execute as few queries as possible</li>
<li id="li4">To know which messages were added</li>
<li id="li5">To commit the messages and merkle trie updates atomically</li>
</ul>

<p id="p22">We could check which messages already exist and filter them out, but that would require an expensive <code>SELECT</code> query (that would probably need to be broken up because you wouldn't want to pass 169,000 parameters). Another idea I had was to insert messages with a unique number, and then afterwards I can query which messages have that unique number since only the new ones would have it. </p>

<p id="p23">The beauty of relational databases (compared to key-value) is they tend to have robust solutions for these kinds of problems. There <em>had</em> to be a way to do this because this pattern is not esoteric. The first thing I learned was how to insert multiple rows with a single <code>INSERT</code> statement:</p>

<pre><code class="sql hljs">-- At least in PostgreSQL, you can pass multiple items to a single insert
INSERT INTO messages_binary (timestamp, group_id, content) VALUES
  ("1", "group1", "binary-blob1"),
  ("3", "group1", "binary-blobb6"),
  ("2", "group1", "binary-blobbbb");
</code></pre>

<p id="p24">This is better than concatenating multiple <code>INSERT</code> statements into one query because it's probably faster, and most importantly we have hope of getting back information about what happened.</p>

<p id="p25">Scouring the docs I discovered the <a href="https://www.postgresql.org/docs/9.5/sql-insert.html#AEN85669"><code>RETURNING</code> clause</a> of an <code>INSERT</code> statement. By default PostgreSQL doesn't return anything when doing <code>INSERT</code> except the number of rows that changed. But if you do <code>INSERT INTO table (value) VALUES (1) RETURNING id</code> it will return the id of the new row.</p>

<p id="p26">The big question was if this did what I wanted: when using an <code>INSERT</code> statement with multiple items and <code>ON CONFLICT DO NOTHING</code>, will it return an array of ids of <em>only the items that were actually inserted</em>? I was suspicious it might return the ids of all the items even if they conflicted (and weren't inserted).</p>

<p id="p27">I wrote a quick script to test the behavior and: <strong>bingo</strong>. <code>RETURNING</code> does exactly what I want. Here's a test:</p>

<pre><code class="sql hljs">INSERT INTO messages_binary (timestamp, group_id, content) VALUES
  (&#39;1&#39;, &#39;group5&#39;, &#39;...&#39;),
  (&#39;2&#39;, &#39;group6&#39;, &#39;...&#39;),
  (&#39;3&#39;, &#39;group7&#39;, &#39;...&#39;)
ON CONFLICT DO NOTHING RETURNING timestamp;
</code></pre>

<p id="p28">When executing this query, if a message with timestamp of <code>1</code> already exists, this will only insert <code>2</code> and <code>3</code> and return an array <code>[{ id: '2' }, { id: '3' }]</code>. Bingo bango bongo.</p>

<p id="p29"><strong><code>RETURNING</code> allows me to reduce all of this work down into a single query</strong>. I can use the results to know exactly which messages were added and update the merkle trie appropriately.</p>

<p id="p30">The new code looks something like this. I'm still <a href="https://twitter.com/jlongster/status/1264943134900981761">auditing the safety</a> of the <code>pg-promise</code> helper:</p>

<pre><code class="javascript hljs"><span class="hljs-comment">// We use a helper from a library `pg-promise` to generate</span>
<span class="hljs-comment">// the multi-value INSERT statement. This will escape values.</span>
<span class="hljs-comment">// <a href='http://vitaly-t.github.io/pg-promise/helpers.html#.insert'>http://vitaly-t.github.io/pg-promise/helpers.html#.insert</a></span>
<span class="hljs-keyword">let</span> stmt = pgp.helpers.insert(
  messages.map(msg =&gt; ({
    timestamp: msg.getTimestamp(),
    group_id: groupId,
    is_encrypted: msg.getIsencrypted(),
    content: msg.getContent()
  })),
  [<span class="hljs-string">'timestamp'</span>, <span class="hljs-string">'group_id'</span>, <span class="hljs-string">'is_encrypted'</span>, <span class="hljs-string">'content'</span>],
  <span class="hljs-string">'messages_binary'</span>
);

<span class="hljs-keyword">let</span> { changes, rows } = <span class="hljs-keyword">await</span> runQuery(
  stmt + <span class="hljs-string">' ON CONFLICT DO NOTHING RETURNING timestamp'</span>
);

rows.forEach(row =&gt; {
  trie = merkle.insert(trie, Timestamp.parse(row.timestamp));
});

<span class="hljs-comment">// Write back the merkle trie…</span>
</code></pre>

<p id="p31">Let's check out the results!</p>

<pre><code class=" hljs">4000 messages
Before: 6.9s
After: .75s

40000 messages
Before: 59s
After: 7.1s
</code></pre>

<p id="p32">You read that right: previously it took <em>59 seconds</em> to process 40000 messages and now it only takes 7.2 seconds. We're able to process 10 times the amount of messages!</p>

<p id="p33"><em>Update: There was an error in the SQL generation causing each piece of data to be larger than needed (the binary blob encoding was wrong) so the generated INSERT statement is about 25% smaller, and 40000 messages is now processed in ~5 seconds.</em></p>

<h2 id="What-about-169,000">What about 169,000?</h2>

<p id="p34">You might be wondering what happened to 169,000, our benchmark? Well, turns out there's still an upper limit. This time we're hitting a PostgreSQL limit and there isn't a quick fix.</p>

<p id="p35">When processing 169,000, the first problem is that, well, node crashes. The <code>pgp.helpers.insert</code> helper from <code>pg-promise</code> causes the crash when passed that number of items. Not exactly sure why, but it's not worth investigating because there are other problems. </p>

<p id="p36">First, 169,000 items requires an upload payload of 21MB. That's unacceptable because the chances of that failing is too large.</p>

<p id="p37">If we scale the benchmark down to 100,000, we get something that gets further. The multi-value <code>INSERT</code> statement that is generated is a <strong>72MB</strong> string. Trying to execute this massive query string simply… hangs the whole server. I'm not sure where the problem is, or if PostgreSQL settings could be tuned to handle it, but again we simply can't handle something of this size.</p>

<p id="p38">The better solution is to page message syncing and have an upper limit per request. A good limit seems to be 40,000 messages. At the size, the upload payload is 5MB and it takes 7 seconds to process (it still generates a 30MB query string which PostgreSQL happily processes!). To process 169,000 messages, we'd send 5 requests each which 40,000 messages (or whatever is leftover). The total time to process all of them would be <code>169000 / 40000 * 7</code> or 29.6 seconds. As long as we display the progress to the user, not bad for such a gigantic changeset.</p>

<p id="p39"><strong>This is the worst case scenario</strong>. We're not normally dealing with timeframes in seconds. The most common syncing operations deal with 10-200 messages which syncs within 20ms. This is absolutely the worst case, like somebody is hitting the API with thousands of changes per second and trying to sync later, which almost never happens. But we should be able to handle it if a user abuses the API.</p>

<h2 id="One-last-improvement">One last improvement</h2>

<p id="p40">Unrelated to the above problem, there is one last improvement I'd like to make. Since the merkle trie is stored in the database, the server needs to fetch it, change it, then store it back. That means no other connections can concurrently change the trie while we're working on it.</p>

<p id="p41">The current solution uses a blunt hammer to solve it: a mutex. The mutex locks per user around the syncing logic, so users can concurrently sync, but if the same user syncs on multiple devices, they will be serialized. This is necessary to avoid race conditions while updating the merkle trie (remember, it's extremely important that it stays in tact).</p>

<p id="p42">It looks like the <a href="https://www.postgresql.org/docs/9.5/transaction-iso.html#XACT-SERIALIZABLE">Serializable Isolation Level</a> for transactions might solve this. You start the transaction with <code>BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE</code>, and PostgreSQL will abort a transaction if it detects that race conditions might occur between them. I'm not 100% sure if it will work with my use case where I read something and write it back later in the same transaction, but if it does, if a transaction fails I'd just restart it. So each syncing process would get serialized. I'd love to hear from you if you know anything about this.</p>

<h2 id="Up-next…">Up next…</h2>

<p id="p43">I haven't thrown the 169,000 benchmark at the client part of syncing yet. The client does more work when syncing because a lot of other things like undo hooks into the system, so there is still a lot to optimize there. I doubt it would handle a case of 169,000 messages right now anyway, but I'm sure it could handle 40,000 messages. I think the idea of paging the syncing into 40,000 blocks would work well though, and would be easy to show feedback to the user of how many messages have been processed so far.</p>

<p id="p44">No matter what, it's great to optimize for the extreme cases. The 9-10x improvement here trickles down to the far smaller cases that make up 95% of the requests. Now a request that took 100ms before will take ~10ms. Snappy!</p>]]></content>
  </entry>
  
  <entry>
    <title>A case study of complex table design</title>
    <link href="https://archive.jlongster.com/case-study-complex-table-design" />
    <published>2020-05-06T00:00:00Z</published>
    <updated>2020-05-06T00:00:00Z</updated>
    <id>https://archive.jlongster.com/case-study-complex-table-design</id>
    <summary type="html"><![CDATA[<p id="p1">I just released a new version of <a href="https://actualbudget.com/">Actual</a> and a big change is a rewrite of the budget table. It might not look like much, but it pays down a lot of technical debt and is a big improvement in many ways. The previous design resulted in a poor user experience despite good intentions. This is a look at how I approach product design and all the considerations you need to think about.</p>

<p id="p2">First, the before (left) and after (right):</p>

<p id="p3"><SideBySide<br />  left={<img src="%ASSETS%/before1.png" />}<br />  right={<img src="%ASSETS%/after1.png" />}<br />/></p>

<p id="p4">On the surface, it's a very subtle change. The headers are now attached to the table and the summary section at the top is separated into its own space. There's another subtle change — can you guess what it is?</p>]]></summary>
    <content type="html"><![CDATA[<p id="p5">I just released a new version of <a href="https://actualbudget.com/">Actual</a> and a big change is a rewrite of the budget table. It might not look like much, but it pays down a lot of technical debt and is a big improvement in many ways. The previous design resulted in a poor user experience despite good intentions. This is a look at how I approach product design and all the considerations you need to think about.</p>

<p id="p6">First, the before (left) and after (right):</p>

<p id="p7"><SideBySide<br />  left={<img src="%ASSETS%/before1.png" />}<br />  right={<img src="%ASSETS%/after1.png" />}<br />/></p>

<p id="p8">On the surface, it's a very subtle change. The headers are now attached to the table and the summary section at the top is separated into its own space. There's another subtle change — can you guess what it is?</p>

<p id="p9">It might be hard to see in the small screenshots, the default font has changed to <a href="https://rsms.me/inter/">Inter</a> from the default system font like San Francisco on Mac. San Francisco is a great font, but it's only available on Macs and it not as readable for data. Inter is a beautiful font made for readability — even when your using the fixed-width feature setting for stuff like transaction amounts.</p>

<p id="p10">Here's a closeup of the difference. Before with San Francisco (top) and after with Inter (bottom):</p>

<p id="p11"><img src="%ASSETS%/before-font.png" alt="" title="" /><br /><img src="%ASSETS%/after-font.png" alt="" title="" /></p>

<p id="p12">They look remarkably similar, but a few small differences make a big impact. The font is <em>slightly</em> taller which helps with readabilty, and the characters aren't quite a squished together. Look at the 1,000.00 amount: with San Francisco (left) the zeros almost bleed together while Inter (right) provides just enough spacing to make them clear.</p>

<p id="p13">There is one major difference in the new design: animations. In the old design, the month was treated as one whole column including the summary and budget values for the month. This was driven home with an animation that slid the entire month column when moving across months:</p>

<p id="p14"><AutoplayVideo url="%ASSETS%/table-animation.mp4" /></p>

<p id="p15">The purpose was to give visual feedback. Without it, it's hard to tell that months actually moved, especially if most of the numbers are the same. It gets worse when you are viewing multiple months at once, a feature that's great for getting a glance at a longer time period at once. Here's an example of multiple months:</p>

<p id="p16"><AutoplayVideo url="%ASSETS%/table-animation2.mp4" /></p>

<p id="p17">The animation is smooth in reality (the video is low-quality). Compare this to no animation:</p>

<p id="p18"><AutoplayVideo url="%ASSETS%/table-animation3.mp4" /></p>

<p id="p19">When the budget values haven't changed, the only thing changing is the month name at the top. I didn't want the feedback to be dependant on having enough different data. However, there's no arguing that the second video is superior for a few reasons:</p>

<ul>
<li id="li2"><p id="p1">It's faster. The user can see new data immediately without waiting for the animation to finish.</p></li>
<li id="li4"><p id="p3">The user can compare data easier. They can keep their eye on the "balance" value for a category while navigating across months and easily see it change. With the animation, it's hard to keep track of the values since they move around.</p></li>
</ul>

<p id="p20">I didn't want to completely give up the visual feedback though. The new design is a compromise that is a much better balance between usability and user experience:</p>

<p id="p21"><AutoplayVideo url="%ASSETS%/table-animation4.mp4" /></p>

<p id="p22">Now the budget table stays fixed while the summary sections animate. This makes it a much more lightweight interaction and feels a lot nicer. A <em>huge</em> benefit is that the implementation is far simpler as well. The previous animation <strong>wreaked havoc</strong> on the DOM structure. Let me explain.</p>

<p id="p23">The problem is the table needs to be scrollable as well. When the user has lots of categories, they need to scroll up and down. So the table can be moved both horizontally and vertically:</p>

<p id="p24"><AutoplayVideo url="%ASSETS%/table-animation5.mp4" /></p>

<p id="p25">We want to use native scrolling, of course, which means we <em>have</em> to put the whole budget table in it's own vertically scrollable container. There's no way around that. In order to slide the months around horizontally, we render the summary views outside of the scrollable area, and inside it each month is rendered as a column. The entire animation looks like this. The purple area is the scrollable container that content is rendered inside of:</p>

<p id="p26"><HypeAnimation id="budgetpage_hype_container" url="%ASSETS%/budget-page.hyperesources/budgetpage_hype_generated_script.js?67066" /></p>

<p id="p27">I was <em>really proud</em> of getting this to work. I still am! The problem is this is a terrible way to renders rows of data. Normally you expect a row of data to be inside a single container which makes a lot of stuff easy. Originally I thought it was worth the sacrifice for a better user experience, but turns out it's not even a great user experience.</p>

<p id="p28">Why is it a terrible way to render rows? Take a closer look at the structure of the DOM we're focused to use. Each column renders a list of data inside of it:</p>

<p id="p29"><HypeAnimation id="rows_hype_container" url="%ASSETS%/rows.hyperesources/rows_hype_generated_script.js?56455" /></p>

<p id="p30">Since a row is composed of multiple disconnected elements, it's impossible to do simple things like a background color on hover. Only each piece of the row will get the hover state. Suddenly <code>.row:hover { background-color: #f0f0f0 }</code> becomes an extraordinary feat of tracking hover state in React and constantly rerendering.</p>

<p id="p31">Even worse, it thrashes rendering whenever anything changes. When rendering lists of data, normally you can <a href="https://reactjs.org/docs/react-api.html#reactmemo">memoize</a> each row and bail on rendering if nothing changes, only causing one row to rerender if something like a category name changes. You can apply the same technique to the above layout, but it needs to do it for <em>every column</em> (in the above example there are 3 columns so there are 3 times as many memoization checks).</p>

<p id="p32">Since the budget table is static in the new design, each row is rendered how you would expect: each row is inside a <code>div</code>. Animating the summaries is easy because there are no scrolling requirements. Take a look again at the new design:</p>

<p id="p33"><AutoplayVideo url="%ASSETS%/table-animation4.mp4" /></p>

<p id="p34">I spent too much damn time working on the old design that <em>isn't even good user experience</em> that I really wish I had taken a step back sooner. At least I have something to write about.</p>

<h2 id="Better-drag-and-drop">Better drag-and-drop</h2>

<p id="p35">Another example of how much simpler the new layout makes everything is drag-and-drop. You can drag categories in the sidebar to reorder them. Previously, it was impossible to render anything across the entire budget table since you're only dragging inside of the sidebar. Here's what it looked like before:</p>

<p id="p36"><AutoplayVideo url="%ASSETS%/dragndrop-old.mp4" /></p>

<p id="p37">Note how the budget table grays out when you start dragging. That was the only thing I could think of since I can't actually move anything outside the sidebar around. There's also weird behavior when expanding a group: the table doesn't expand with it.</p>

<p id="p38">Here's the new implementation. While it doesn't beautifully slide content around, it's much more crisp and clear. The blue line renders across the entire budget table, expanding and collapsing groups works with all the data as well, and it's just better:</p>

<p id="p39"><AutoplayVideo url="%ASSETS%/dragndrop-new.mp4" /></p>

<h2 id="Other-improvements">Other improvements</h2>

<p id="p40">This work led to a couple other small improvements. You can now collapse the summary view if you want more space for the categories. Combined with the multiple month view, you can really get a nice condensed view of your budget:</p>

<p id="p41"><AutoplayVideo url="%ASSETS%/summary-collapse.mp4" /></p>

<p id="p42">Because the new layout is so much more performant, the maximum number of months viewed at once has been bumped up to 6 from 4. Just look at this beautiful monster:</p>

<p id="p43"><img src="%ASSETS%/6months.png" alt="" title="" /></p>

<p id="p44">Ignore the inconsistent widths of the row borders, I had to <em>zoom out</em> to fit all of this which causes a few glitches.</p>

<p id="p45">This release includes a lot of other changes like the ability to attach notes to categories and months. Check out the <a href="http://actualbudget.com/release-notes/#0.0.119">release notes</a>, and you can always try a <a href="https://app.actualbudget.com/">demo of the app</a>.</p>]]></content>
  </entry>
  
  <entry>
    <title>Joining Stripe</title>
    <link href="https://archive.jlongster.com/joining-stripe" />
    <published>2020-04-06T00:00:00Z</published>
    <updated>2020-04-06T00:00:00Z</updated>
    <id>https://archive.jlongster.com/joining-stripe</id>
    <summary type="html"><![CDATA[<p id="p1">I made a big change recently: I joined <a href="https://stripe.com/">Stripe</a> as a developer on a team that works on the dashboard. Today is my first day!</p>

<p id="p2">First, let me say that I'm really excited. I talked to <a href="https://twitter.com/SlexAxton">Alex Sexton</a> 5 years ago about joining Stripe, but the timing wasn't quite right. It's always been on my list of companies (which is quite small) that I would work for if I was to make a change. The problem is interesting, lots of great people work there, and they are remote friendly.</p>

<p id="p3">This comes with a tinge of sadness, though. Over 3 years ago I quit my job to explore contracting and focus more on building a product. That product eventually become <a href="https://actualbudget.com/">Actual</a>, and I focused hard on it for the last year and a half. "Getting a job" feels like quitting.</p>

<p id="p4">I've struggled to identify my goals. Am I a startup? Should I get funding? Do I need to find a cofounder? What happens in 5 years?</p>]]></summary>
    <content type="html"><![CDATA[<p id="p1">I made a big change recently: I joined <a href="https://stripe.com/">Stripe</a> as a developer on a team that works on the dashboard. Today is my first day!</p>

<p id="p2">First, let me say that I'm really excited. I talked to <a href="https://twitter.com/SlexAxton">Alex Sexton</a> 5 years ago about joining Stripe, but the timing wasn't quite right. It's always been on my list of companies (which is quite small) that I would work for if I was to make a change. The problem is interesting, lots of great people work there, and they are remote friendly.</p>

<p id="p3">This comes with a tinge of sadness, though. Over 3 years ago I quit my job to explore contracting and focus more on building a product. That product eventually become <a href="https://actualbudget.com/">Actual</a>, and I focused hard on it for the last year and a half. "Getting a job" feels like quitting.</p>

<p id="p4">I've struggled to identify my goals. Am I a startup? Should I get funding? Do I need to find a cofounder? What happens in 5 years?</p>

<p id="p5">The truth is: I love building products. I love talking with customers and solving their needs. I love figuring out how to market it. But when it comes down to it, building a business is not something I want to do. There's <em>so</em> much work involved and it's just too stressful for my family.</p>

<p id="p6">I had a goal of reaching 1000 users which would start to be financially sustainable. It seems like a modest goal, but I've learned how hard it is to actually sell software. I've only reached 155 users, and there's tons of reasons why I haven't grown faster. The biggest problem is trying to do this alone — besides amplyifying my weaknesses, there's simply too much work to do.</p>

<p id="p7">I didn't want to do it alone, but I failed at finding a cofounder or somebody who would be significantly involved without funding. I thought about getting funded, and even talked to a few investors, but it exposed my feelings deep down that I didn't really want to be in the business of building a business.</p>

<p id="p8">I just want to make a great product that users love. And I'm going to keep doing that.</p>

<h2 id="What-does-this-mean">What does this mean for Actual?</h2>

<p id="p9">In short: <a href="https://actualbudget.com/">Actual</a> is still very much alive, but I won't have as much time to work on it. Let me try to explain why that might be a good thing.</p>

<p id="p10">Actual is a personal finance tool focusing on providing the most power and flexibility in the simplest tool possible. The simplicitly is expressed in a fast and streamlined UX, and the goal is to provide powerful tools for managing your finances how you like (choose your budgeting method, write custom reports, and more).</p>

<p id="p11">Actual is the hardest thing I've ever done. I like to rethink things from the bottom up, and Actual is no exception. Inside of Actual is a <a href="https://www.dotconferences.com/2019/12/james-long-crdts-for-mortals">syncing engine</a> that keeps all of your data local and uses smart techniques for robustly syncing changes across devices. There's so much cool stuff in there, like end-to-end encrypting the changes so you have full privacy.</p>

<p id="p12">The entire app is what's called "local-first": literally everything is happening against local data, and syncing to the cloud is just a background service that happens when it can. This changes everything.</p>

<p id="p13">One of the biggest advantages is the app is <em>super fast</em>. Since everything is local, reading and writing data is stupidly fast. You can try the app for yourself <a href="https://static.actualbudget.com/app">here</a>.</p>

<p id="p14">It's all custom built because nothing out there met all my requirements. And I don't regret building it at all. I'm confident that I wouldn't be able to do everything I've done with anything else.</p>

<p id="p15">Just a sampling: <a href="https://twitter.com/jlongster/status/1149762749683159041">full undo/redo</a> (like, the <em>real</em> thing, not some cheap version), <a href="https://twitter.com/jlongster/status/1241040807093768194">running the app in the background</a> that multiple tabs/windows connect to, using the app <a href="https://twitter.com/jlongster/status/1237108913100775426">with literally nothing going over the network</a>, and lots more.</p>

<p id="p16">Note how all of these benefits are <em>user-facing</em>. I wouldn't invest so much in this if I didn't think it made a huge impact on the capabilities of the product itself. A driving motivation for this architecture is the ability to write <strong>custom reports</strong> in a simple language, and it'll be easy since it has access to all your data right there.</p>

<p id="p17">The thing I'm <strong>most proud of</strong> is that the syncing engine has reached almost 100% stability. Any problems are now caused by external factors, like a bug in the code that initially downloads your data when logging in. The layer that actually commits changes and syncs them across devices is nearly 100% reliable: if you make a change, sync to the cloud, and sync other devices, you are guaranteed to see the same data. And it's all totally seamless.</p>

<p id="p18">Unfortunately, it's costly to build things up from scratch. Here's what surprised me: writing the code itself didn't take too long. However, when you've done something different, it takes a <em>lot</em> of work to explain it to users and build up a coherent UX.</p>

<p id="p19">For example, now that everything is local, what does the login process look like? Do I authenticate a user, and then get them to manually copy over data from another device to get started? Well… of course not. I built a system for tracking which files are available to download, but what happens once you're all set up if you use "reset sync" on one device? "Reset sync" deletes all syncing info and starts fresh, but now other devices won't sync and need to be re-setup.</p>

<p id="p20">It sure would be nice if I didn't have to worry about all this, but moving fully to the cloud just gets rid of too many nice features. I had to rebuild the whole user experience for managing data, write documentation, and explain all of this to users, which took far too long.</p>

<p id="p21">I want to focus more on the features of Actual itself. And here's where my new role might help: now that I'm not betting my family's income on Actual, I don't need to worry about a <em>lot</em> of things. I can focus more on the product itself.</p>

<p id="p22">It impacts all decisions around Actual. For example, I was going to raise the prices to somewhere around $7/month for annual plans when bank syncing launches. I still plan on raising prices, but not as much.</p>

<p id="p23">Bank syncing is still going to launch. Custom reports is still going to launch. I have a lot of improvements already done and coming soon. I'm still working on scheduled transactions. Actual isn't going anywhere.</p>

<p id="p24">There's no getting around the fact that I won't have as much time for Actual. To my existing and future customers: it's up to me to prove that I am still going to work on it and make it worth your subscription. I fully intend to do so. There's no way I'm going to let a multi-year investment into such an awesome product die.</p>]]></content>
  </entry>
  
  <entry>
    <title>The wonderful sound of an atomic commit</title>
    <link href="https://archive.jlongster.com/wonderful-sound-atomic-commit" />
    <published>2020-02-11T00:00:00Z</published>
    <updated>2020-02-11T00:00:00Z</updated>
    <id>https://archive.jlongster.com/wonderful-sound-atomic-commit</id>
    <summary type="html"><![CDATA[<p id="p1">There's a horrifying realization as you work with software of how fragile it is. Failures can happen almost anywhere, and a lot of code isn't equipped to deal with it.</p>

<p id="p2">We can at least assume, in JavaScript, that when your code is executing it will keep executing until it either ends or waits for something. Your OS could still crash so JavaScript technically isn't "run-to-completion". That's relatively safe though; your program would stop in the middle of some in-memory computations which isn't a big deal.</p>

<p id="p3">No, the real hairy stuff happens whenever you need to access the outside world. Which of course, all practical programs do all the time. If you need to read or write from a file or the network, you pretty much have to assume it's going to fail. Which means almost all code paths in JavaScript that use <code>await</code> to wait on something is going to fail. It's arduous and difficult.</p>

<p id="p4">What's worse is anything interesting involves <em>multiple</em> interactions with the outside world. Let's say you have a bunch of static files for your blog and you want to add "© James Long 2020" to the bottom of all of them (you don't use a static site generator because you can't afford the disk space of pulling from npm). You write a shell script to loop over the files and append the text, <em>but then your script errors in the middle of it</em>! Your left with a half-modified set of files… and there's no easy way back to the good state of things.</p>]]></summary>
    <content type="html"><![CDATA[<p id="p1">There's a horrifying realization as you work with software of how fragile it is. Failures can happen almost anywhere, and a lot of code isn't equipped to deal with it.</p>

<p id="p2">We can at least assume, in JavaScript, that when your code is executing it will keep executing until it either ends or waits for something. Your OS could still crash so JavaScript technically isn't "run-to-completion". That's relatively safe though; your program would stop in the middle of some in-memory computations which isn't a big deal.</p>

<p id="p3">No, the real hairy stuff happens whenever you need to access the outside world. Which of course, all practical programs do all the time. If you need to read or write from a file or the network, you pretty much have to assume it's going to fail. Which means almost all code paths in JavaScript that use <code>await</code> to wait on something is going to fail. It's arduous and difficult.</p>

<p id="p4">What's worse is anything interesting involves <em>multiple</em> interactions with the outside world. Let's say you have a bunch of static files for your blog and you want to add "© James Long 2020" to the bottom of all of them (you don't use a static site generator because you can't afford the disk space of pulling from npm). You write a shell script to loop over the files and append the text, <em>but then your script errors in the middle of it</em>! Your left with a half-modified set of files… and there's no easy way back to the good state of things.</p>

<p id="p5">We can find solace in a few things that bring sanity to this world though. One of those things is <em>atomic</em> behavior — something which you can rely on either succeeding or failing entirely. There's no "half states". Anything providing atomic behavior gives me warm fuzzies and I like to go to sleep holding it.</p>

<p id="p6">Going back to our file example: one solution would be to copy the file somewhere temporary, modify them there, and move them back. You'd have to move them back under a new name, so let's assume you have "posts" and the new modified directory will be "posts-2":</p>

<pre><code class=" hljs">mv /tmp/modified-posts ./posts-2
</code></pre>

<p id="p7">If "posts-2" exists after your script ends, you know all the files in it are modified. Unfortunately even the question <a href="https://unix.stackexchange.com/questions/322038/is-mv-atomic-on-my-fs">"is mv atomic on my fs?"</a> is complicated to answer, but generally speaking it is.</p>

<p id="p8">This example might not seem so important, so let's improve it. Say you're working on a server that is serving your blog files. You're so interesting and everybody wants to read your stuff, so you get 1000 requests per second. You don't want any down time, and you never want any "halfway states". How do you modify the contents of the <code>posts</code> directory that the server is reading from?</p>

<p id="p9">If <code>posts</code> is a regular directory, you can't. You're going to need at least two commands, one to move that directory away and another to move the new one in. The answer is something you have to do from the beginning: make <code>posts</code> a symlink. With symlinks, you can atomically change what they link to:</p>

<pre><code class=" hljs">// The world will change in a single instant, atomically
ln -sf ./posts ./files/modified-posts
</code></pre>

<p id="p10">I don't know about you, but whenever I do something atomic, I can almost hear a BOOM as it's committed.</p>

<p id="p11"><code>-s</code> means create a symbolic link, and <code>-f</code> means to overwrite an existing symlink (on macOS, it's different for each OS). The nice thing is that the filesystem guarantees that this is an atomic operation (as far as I know). Symlinks are a great way to allow data to change: you can go off and do a bunch of work on files and then update the symlink to the new location if everything succeeds.</p>

<p id="p12">Sadly, so many tools don't work atomically. Imagine if you could ctrl+c to stop a <code>npm install</code> and your filesystem was exactly the way it was before you started the command? I <em>love</em> anything that allows me to make mistakes and then backtrack to where I was before.</p>

<p id="p13">There's a relationship to immutability in programming languages here, where immutability forces you to make modifications somewhere else and then swap it atomically so that the world sees it. In fact <a href="https://nixos.org/nix/manual/">Nix</a>, an immutable filesystem, is exploring exactly that.</p>

<p id="p14">I've gone off on a tangent. My sorrow for more reliable tooling finds solace in a place where atomicity is par for the course: databases.</p>

<h2 id="My-real-world-use-ca">My real-world use case</h2>

<p id="p15">What inspired me to write this post is recently I worked on improving the reliability of syncing in <a href="https://actualbudget.com/">Actual</a>. Actual is a local app and keeps all data locally, and syncs changes across devices. It's critical for the syncing layer to be as robust as possible, otherwise it could corrupt your data.</p>

<p id="p16">Internally, Actual uses <a href="https://www.sqlite.org/index.html">sqlite</a> to store data. The best thing about SQL databases is they offer "transactions" as way to deal with multiple changes across time (the A in <a href="https://en.wikipedia.org/wiki/ACID">ACID</a> stands for Atomic). Using a transaction, you can make a bunch of changes and then <code>COMMIT</code> them atomically to the database, or use <code>ROLLBACK</code> to revert those changes. The important part of <code>COMMIT</code> is it <a href="https://www.sqlite.org/atomiccommit.html">guarantees an atomic commit</a> to the filesystem.</p>

<p id="p17">The syncing process goes something like this:</p>

<pre><code class="javascript hljs"><span class="hljs-keyword">let</span> messages = getNewMessages(receivedMessages);
<span class="hljs-keyword">let</span> clock = getClock()

messages.forEach(msg =&gt; {
  db.runQuery(
    <span class="hljs-string">`UPDATE ${msg.table} SET ${msg.column} = ? WHERE id = ?`</span>,
    [msg.value, msg.id]
  );
  db.runQuery(
    <span class="hljs-string">`INSERT INTO messages_crdt (timestamp, table, id, column, value) VALUES (?, ?, ?, ?, ?)`</span>,
    [msg.timestamp, msg.table, msg.id, msg.column, msg.value]
  );

  clock = insertMessage(clock)
});

db.runQuery(
  <span class="hljs-string">'INSERT OR REPLACE INTO messages_clock (id, clock) VALUES (1, ?)'</span>,
  serializeClock(clock)
)
</code></pre>

<p id="p18">Don't worry too much about the algorithm (it's greatly simplified) but we need to make several changes and they <strong>have</strong> to be applied atomically — all at the same time. We change the actual data, insert the message into the CRDT, and after processing all the messages store the new clock. If any of these changes got comitted without finishing the process, the syncing data would be corrupted.</p>

<p id="p19">We can do this by using a <strong>transaction</strong>, which is easy:</p>

<pre><code class="javascript hljs">db.execQuery(<span class="hljs-string">'BEGIN'</span>);
<span class="hljs-comment">// … make all my changes …</span>
db.execQuery(<span class="hljs-string">'COMMIT'</span>)
</code></pre>

<p id="p20">I can't help it — whenever I see a <code>COMMIT</code> I can't help but imagine it makes a joyful BOOM sounds when it successfully commits. It's such a nice thing to hear.</p>

<p id="p21">That is how syncing has worked in Actual up until the next update. Unfortunately, there's a sneaky problem with the above code. We have to remember how fragile everything is: what happens if one of the SQL statements fails?</p>

<p id="p22">We only want all of these updates to commit if <em>all of them succeeds</em>, otherwise we want to <em>rollback</em> the entire set of changes. This way, the sync update is very much an atomic block of changes. It either happens or it doesn't.</p>

<p id="p23">Unfortunately, if a sqlite query fails it stops executing, but the transaction is left open. Some future code will eventually do a <code>COMMIT</code> for their own stuff and end up committing some leftover changes accidentally.</p>

<p id="p24">My problem in Actual was even worse for reasons I'll only summarize: <code>runQuery</code> used to be async and I wasn't <code>await</code>ing on it on purpose. I don't support async transactions because it requires so much machinery (you need to block all async work until you're done), and sync is so much simpler. I was firing off updates linearly which works if all queries are successful, but if one fails my code would happily continue to execute and run all queries regardless.</p>

<p id="p25">I use <a href="https://github.com/JoshuaWise/better-sqlite3"><code>better-sqlite3</code></a> which is a <em>freaking</em> fantasic libary because it's synchronous (go read the reasons why). To fix my problems I need to do a few things. First, I need to make <code>runQuery</code> synchronous, because <code>better-sqlite3</code> is sync anyway (it was async for reasons I won't go into).</p>

<p id="p26">Now that <code>runQuery</code> is sync, it will properly stop executing when a query fails. But I still need to do a <code>ROLLBACK</code> to make sure no updates are leftover. The resulting code looks like this:</p>

<pre><code class="javascript hljs">db.execQuery(<span class="hljs-string">'BEGIN'</span>);
<span class="hljs-keyword">try</span> {
  <span class="hljs-comment">// … make all my changes …</span>

  <span class="hljs-comment">// BOOM</span>
  db.execQuery(<span class="hljs-string">'COMMIT'</span>)
}
<span class="hljs-keyword">catch</span>(e) {
  <span class="hljs-comment">// Yeesh, nevermind</span>
  db.execQuery(<span class="hljs-string">'ROLLBACK'</span>)
  <span class="hljs-keyword">throw</span> e
}
</code></pre>

<p id="p27">Even better, <code>better-sqlite3</code> comes with a builtin <code>transaction</code> function which does exactly this (and also supports nested transactions) so the code ends up looking like this:</p>

<pre><code class="javascript hljs"><span class="hljs-keyword">let</span> run = db.transaction(() =&gt; {
  <span class="hljs-comment">// … make all changes …</span>
})

run()
</code></pre>

<p id="p28">If an exception is thrown within <code>db.transaction</code>, it will automatically rollback the transaction. I've run a bunch of property tests against the newly improved syncing layer and it's had no problems, even if things fail sometimes.</p>

<p id="p29">It feels <em>really</em> good to harden the syncing layer. You could throw your computer out the window while it's syncing and it would recover fine from it. Your hardware might not though. </p>

<p id="p30"><em>All of these changes will be available in the next update 0.0.114 of <a href="https://actualbudget.com/">Actual</a>, coming this week</em></p>]]></content>
  </entry>
  
  <entry>
    <title>Thinking about growth and profit</title>
    <link href="https://archive.jlongster.com/thinking-growth-profit" />
    <published>2020-01-09T00:00:00Z</published>
    <updated>2020-01-09T00:00:00Z</updated>
    <id>https://archive.jlongster.com/thinking-growth-profit</id>
    <summary type="html"><![CDATA[<p id="p1"><em>I posted a follow-up <a href="https://twitter.com/jlongster/status/1215661904804425734">twitter thread</a> with a final spreadsheet and learnings.</em></p>

<p id="p2">In my <a href="https://archive.jlongster.com/analyizing-profit-metrics">last post</a> I showed my process for forecasting costs and profits for <a href="https://actualbudget.com/">Actual</a> based on various metrics. This helps me find much-needed answers to questions like "how much cost for acquiring users can I afford?"</p>

<p id="p3">I'm new to this and I learned a lot from my first attempt. There's still a lot to improve on though. This post goes into a few more things I've learned based on feedback and more research.</p>

<p id="p4">Some context: I launched Actual a year ago, but really only started focusing on growth since last September (4 months ago). It's a very early product and I'm a solo founder.</p>]]></summary>
    <content type="html"><![CDATA[<p id="p9"><em>I posted a follow-up <a href="https://twitter.com/jlongster/status/1215661904804425734">twitter thread</a> with a final spreadsheet and learnings.</em></p>

<p id="p10">In my <a href="https://archive.jlongster.com/analyizing-profit-metrics">last post</a> I showed my process for forecasting costs and profits for <a href="https://actualbudget.com/">Actual</a> based on various metrics. This helps me find much-needed answers to questions like "how much cost for acquiring users can I afford?"</p>

<p id="p11">I'm new to this and I learned a lot from my first attempt. There's still a lot to improve on though. This post goes into a few more things I've learned based on feedback and more research.</p>

<p id="p12">Some context: I launched Actual a year ago, but really only started focusing on growth since last September (4 months ago). It's a very early product and I'm a solo founder.</p>

<p id="p13">First, a couple takeaways from responses to my previous post:</p>

<ul>
<li id="li1">Let's be more pessimistic about churn and say 10%. </li>
<li id="li2">Costs are simple in this model, but you want to all costs. I focus on Plaid costs because that will be the majority of my costs, but in a real forecast make sure to include everything like Stripe's fees. Marketing should be part of acquisition costs, but I'm doing any marketing right now.</li>
<li id="li6"><p id="p3">A reader asked an interesting question: how many people signed up for a trial without ever running the app (and never costing anything)? I can't find the original response, but they said to expect almost half of trials to never do anything. I ran the numbers and so far on my 3 months of data, and:</p>

<p id="p4"><strong>Holy crap</strong>. Out of 1025 total trials, only 508 actually ran the app and logged in which is 49%. This is a little skewed because I'm transitioning away from a free plan which doesn't track users, so some of those earlier trial conversions aren't tracked. But it's probably not far off.</p>

<p id="p5">For simplicity, I'll assume all trial signups cost the same.</p></li>
</ul>

<h2 id="The-cost-of-growth">The cost of growth</h2>

<p id="p14">Near the end of my last post, I asked the question <a href="https://archive.jlongster.com/analyizing-profit-metrics#what-does-it-take-to-hit-10kmonth">"What does it take to hit $10k/month?"</a> Cranking up growth surprised me because I found myself <em>spending</em> more and more money each month, spending $11,714 after 16 months with a 40% growth rate (<a href="https://docs.google.com/spreadsheets/d/1Ld8iq3XPCZHBjyMb6TcfQWg2Agw96yqLgoS_DLgeUTA/edit#gid=1826721471">link to spreadsheet</a>):</p>

<p id="p15"><SpreadsheetWithGraph src="https://docs.google.com/spreadsheets/d/e/2PACX-1vSC7DIkgQXlIt6ZpaDWQDpMlAd1__dlW2SRh8jEPzMsDNVTpMTWOJUH-Q5Fc_qpWfU1iVyWn2Gc8tw7/pubhtml?gid=1826721471&amp;single=true&amp;widget=true&amp;headers=false" /></p>

<p id="p16">It shouldn't have surprised me though. Growth costs money, and simply put: in this scenario, I'm spending more than I'm making. The problem is I'm assuming a 40% growth for all time and given the cost per trial, conversion rate, and pricing I can't sustain that.</p>

<p id="p17">All this money spent is really an <em>investment</em>. I may have spent a total of $42,746.16, but now I have 7834 subscribers. Let's guess at a customer lifetime of 12 months, at $7/months I will eventually make <code>7834 * 7 * 12</code> or $658,056. That's a 1539% return on investment.</p>

<p id="p18">In reality, constant 40% growth is not going to happen. Eventually growth will slow and I will start making a return. If it doesn't slow, that's amazing and you're onto something big. Here's what happens if I had 40% growth for 6 months, and then it slows down to 5%:</p>

<p id="p19"><SpreadsheetWithGraph graphSrc="https://docs.google.com/spreadsheets/d/e/2PACX-1vSC7DIkgQXlIt6ZpaDWQDpMlAd1__dlW2SRh8jEPzMsDNVTpMTWOJUH-Q5Fc_qpWfU1iVyWn2Gc8tw7/pubchart?oid=904572546&amp;format=interactive" /></p>

<p id="p20">As growth slows, the costs I pay for trial users goes down and profits go up. If you wanted to, you could model growth as a curve over time and see how that effects cash flow. That would reflect reality since growth isn't constant.</p>

<p id="p21">If you wanted to focus on growth for a long time, you'd either need to take a loan/investment or tweak other parameters (like pricing, conversion, etc) to support it.</p>

<h2 id="Annual-plans-are-kil">Annual plans are killer</h2>

<p id="p22">I <a href="https://archive.jlongster.com/analyizing-profit-metrics#annual-plans">briefly looked</a> at annual plans at the end of the last post. With an annual plan, the user pays for an entire year upfront. Obviously, this provides a lot more cash flow.</p>

<p id="p23">It's pretty incredible what this extra cash allows. Check out the (extremely hypothetical) 40% growth for 16 months (<a href="https://docs.google.com/spreadsheets/d/1Ld8iq3XPCZHBjyMb6TcfQWg2Agw96yqLgoS_DLgeUTA/edit#gid=382759458">link to spreadsheet</a>):</p>

<p id="p24"><SpreadsheetWithGraph src="https://docs.google.com/spreadsheets/d/e/2PACX-1vSC7DIkgQXlIt6ZpaDWQDpMlAd1__dlW2SRh8jEPzMsDNVTpMTWOJUH-Q5Fc_qpWfU1iVyWn2Gc8tw7/pubhtml?gid=382759458&amp;single=true&amp;widget=true&amp;headers=false" /></p>

<p id="p25">I never even lose money — I make $1012 the first month subscribers are charged. Sure, some of this money needs to be reserved to cover the operational costs of the user for a year, but it's most likely a small fraction of that.</p>

<p id="p26">In a reply to my previous post, one person mentioned that if you offer monthly and annual plans, it's common for 1/3 of your users to be on the annual plan. I'll leave the modeling of this mixture up to you, but it's not hard to guess that even with only a 1/3 you'd have a lot more freedom with cash flow.</p>

<p id="p27">Annual plans are killer.</p>

<h2 id="Just-how-is-growth-a">Just how is growth and profit related?</h2>

<p id="p28">Even with the annual plan, if you crank up growth you'll still end up in the negative after 16 months. It may take a growth rate of 398%, but it still happens!</p>

<p id="p29">The math nerd in me had to figure out just how growth and profits are related. Obviously there is an inflection point where growth overtakes costs, but how do we solve for that? <strong>You don't <em>really</em> need to understand all this</strong>, but at least try to understand why I <a href="#redefining-growth">had to redefine growth</a> in the next section and then skip to the <a href="#bedtime-epiphany">simpler math</a>.</p>

<p id="p30">It would be nice to know, given acquisition cost, conversion rate, and pricing, just how much growth can I afford? A better way to put it: <strong>how much growth will I get if I reinvest all profits?</strong></p>

<p id="p31">It took me a while to work through it. The first thing I did was add the ratio of <code>total subscribers / new subscribers</code>, and play with similar relationships, but none of those help much. It seemed clear that is <em>is</em> a relationship, i.e. I should be able to find a value based on growth.</p>

<p id="p32">Think about what we need: the <em>cost</em> needs to have a direct relationship with <em>revenue</em>. We need a formula that involves growth, and then we can solve for growth. Given all the other metrics, for what growth value X does <code>revenue = cost</code>?</p>

<p id="p33">Let's define <code>revenue = cost</code> in terms of growth. Right now, our <code>growth</code> variable defines growth in terms of new trials. First, let's figure out <code>cost</code> based on the number of trials. It's a compounding rate of growth, so we need to learn the equation for compounding interest. It's not that hard:</p>

<p>\(trials = 300 * growth^x\)</p>

<p id="p34">where <code>x</code> is the month. We start with 300 trials and it increases with <code>growth</code> (for 7% <code>growth = 1.07</code>). Now we can determine the amount of trials at any point in time given growth, which means we know the cost at any point in time. Just multiply it by conversion and trial cost. Given a $1/trial and a 6% conversion rate:</p>

<p>\(cost = 300 * growth^x * 1 * .06\)</p>

<p id="p35">This will give you the cost of trials at month <code>x</code>. Note: to keep things simple we're ignoring the "total cost" and using the cost of trials only. It wouldn't hard to also account for the cost of existing subscribers (which is much smaller) and I'll do that at the end.</p>

<p id="p36">Let's look at revenue. Revenue should be number of subscribers at the beginning of the month multiplied by the plan price. Seems simple, right? The problem is that the subscribers count currently is generated with the formula <code>last_month_subscribers + new_subscribers</code>, with new subscribers coming from the converted number of trials. There's no way to calculcate <code>last_month_subscribers</code> numerically: we can get the number of <em>new</em> subscribers last month, but not the total subscribers.</p>

<p id="p37">To get the total subscribers we have to sum the generated subscribers from each month. From there, we can finally generate <code>revenue</code> but multiplying the subscriber count by the prive (using $7 here):</p>

<p>\({'revenue = \\sum\\limits_{i=1}^x 300 * growth^x * .06 * 7 '}\)</p>

<p id="p38">This says "for month 3, <code>revenue = f(1) + f(2) + f(3)</code>" where <code>f</code> is the formula above that generated the revenue.</p>

<p id="p39">But that doesn't take into subscriber churn. Churn is based on the <em>previous</em> month's subscribers… this is getting complicated fast. Even if I figure out the right revenue formula, <em>I still have to solve for growth</em>.</p>

<h3 id="Redefining-growth">Redefining growth</h3>

<p id="p40">It's way too complicated to measure growth by number of trials and calculate everything else from there. And there's no reason to do that! It's much better if we <a href="https://twitter.com/jeremydeanlakey/status/1214971624996274176">redefine growth</a>.</p>

<p id="p41">Instead, define growth as <code>current subscribers / previous subscribers</code>, or the growth of subscribers over time. This makes the math far simpler.</p>

<p id="p42">We can easily calculate everything else from there. First, use the compounding formula again to determine the total number of subscribers (assuming we start with 15 subscribers):</p>

<p>\({'subscribers = 15 * growth^{x-1}'}\)</p>

<p id="p43">The reason we use <code>x - 1</code> is because we want to know the number of subscribers at the <em>beginning</em> of the month, so ignore the new subscribers of the current month. Revenue is easy (with $7/month price):</p>

<p>\({'revenue = 15 * growth^{x-1} * 7'}\)</p>

<p id="p44">For cost, we need to know the number of <em>new</em> subscribers. We just subtract the difference between previous and current month:</p>

<p>\({'new\\_subscribers = 15 * growth^x - 15 * growth^{x-1}'}\)</p>

<p id="p45">Given the amount of new subscribers, we can generate the amount of trials that occurred by dividing the conversation rate. </p>

<p>\({'trials = (15 * growth^x - 15 * growth^{x-1}) / .05'}\)</p>

<p id="p46">Now it's easy to calculate cost by multiplying trials by cost per trial ($1):</p>

<p>\({'cost = (15 * growth^x - 15 * growth^{x-1}) / .05 * 1'}\)</p>

<p id="p47">We have both sides, now we need to solve the equation!</p>

<p>\({'15 * growth^{x-1} * 7 = (15 * growth^x - 15 * growth^{x-1}) / .05 * 1'}\)</p>

<p id="p48">Solving for <code>growth</code> will give us a number that represents if we reinvested all revenue into growth. It's easier than it looks to solve: divide both sides by <code>growth^x-1</code> and you reduce it into the far simpler:</p>

<p>\( 15 * 7 = (15 * x - 15) / 0.05 \)</p>

<p id="p49">Which gives us:</p>

<p>\( x = 1.35 \)</p>

<p id="p50">Bam! If I use a growth rate of 35%, cost equals revenue and profit is 0 each month. I'm reinvesting all revenue into growth. If growth were higher, I need money outside profit to cover it. This is confirmed by updating the spreadsheet to use subscriber growth, plugging in 35% growth and plotting it (<a href="https://docs.google.com/spreadsheets/d/1Ld8iq3XPCZHBjyMb6TcfQWg2Agw96yqLgoS_DLgeUTA/edit#gid=1848481227">link to spreadsheet</a>):</p>

<p id="p51"><SpreadsheetWithGraph graphSrc="https://docs.google.com/spreadsheets/d/e/2PACX-1vSC7DIkgQXlIt6ZpaDWQDpMlAd1__dlW2SRh8jEPzMsDNVTpMTWOJUH-Q5Fc_qpWfU1iVyWn2Gc8tw7/pubchart?oid=675042943&amp;format=interactive" /></p>

<p id="p52">You can't see the blue line which is costs because it's <em>exactly equal</em> to revenue. Notice how profit is flat the entire time. If you look at the <a href="https://docs.google.com/spreadsheets/d/1Ld8iq3XPCZHBjyMb6TcfQWg2Agw96yqLgoS_DLgeUTA/edit#gid=1848481227">spreadsheet</a>, profits are all 0.</p>

<h3 id="Bedtime-epiphany">Bedtime epiphany</h3>

<p id="p53">After I did all of this I couldn't shake the feeling that I over-complicated it. All I needed was the growth, which is the rate of the current month's subscribers to the previous month's. When I went to bed I pulled out my calculator and did a few more tests.</p>

<p id="p54">If I wanted growth, I need to solve something like this:</p>

<p>\({' growth = \\frac{subscribers + new\\_subscribers}{subscribers} '}\)</p>

<p id="p55">If I know customer acquisition costs (CAC), can't I just plug that in and calculate the amount of new subscribers? I can figure out CAC based on conversion rate (5%) and the cost of each trial ($1):</p>

<p>\({' CAC = 1 / .05 * 1 '}\)</p>

<p id="p56"><code>1 / .05</code> gives us the amount of trials needed to convert one customer, and we multiply that by the trial cost. In this case the CAC is $20. Well… can't we just use that to figure out exactly how many new subscribers we will get be reinvesting all revenue?</p>

<p>\({' new\\_subscribers = \\frac{revenue}{CAC} '}\)</p>

<p id="p57">Revenue is easy: just <code>subscribers * 7</code> (since it's $7/month). Plugging it all in:</p>

<p style={{fontSize:'1.3em'}}>\({' growth = \\frac{subscribers + \\frac{subscribers * 7}{1 / .05 * 1}}{subscribers} '}\)</p>

<p id="p58">And look, subscribers divides itself out! Leaving: </p>

<p style={{fontSize:'1.3em'}}>\({' growth = 1 + \\frac{7}{1 / .05 * 1} = 1.35 '}\)</p>

<p id="p59">Wow, that was a lot simpler! I'm sure you could transform both of my final equations into each other. This was a much more straight-forward approach though, and it took me a while to get there.</p>

<p id="p60">It would be really easy to include other costs as well. So far I've left out an estimated $0.75/month in Plaid costs per subscriber, but use <code>subscribers * (7 - .75)</code> in the equation for revenue and it'll account for that. If we account for total costs, the growth rate is 31.25%.</p>

<p id="p61">This leaves us with the final equation:</p>

<p style={{fontSize:'1.3em'}}>\({' growth = 1 + \\frac{price - subscriber\\_cost}{1 / conversion\\_rate * CAC } '}\)</p>

<h2 id="Run-the-numbers">Run the numbers</h2>

<p id="p62">I didn't need to understand all that math; I could have just tweaked values in excel. But it's nice to know how it's all related, and to know the upper bound of growth given the other numbers. Here are some other scenarios:</p>

<ul>
<li id="li7">If I only allow trials to connect a single account, trial costs become $0.50 instead of $1. Max growth rate: 62%</li>
<li id="li8">If conversion goes up to 8%, max growth rate is 50%</li>
</ul>

<p id="p63">I could go on. You probably don't need to focus on this, it's just a good thing to understand. Note how churn rate isn't part of this because <code>growth</code> measures the amount of subscribers growth after churn. You would need some different equations to play with churn rate. (Edit: or just make a "real growth" variable that is <code>growth - churn</code>)</p>

<p id="p64">Or just throw some formulas into a spreadsheet and play with them until they look right.</p>

<script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>

<script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>]]></content>
  </entry>
  
</feed>
