<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xml:base="https://dvdcst.dev/" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>dvdcst.dev</title>
    <link>https://dvdcst.dev/</link>
    <atom:link href="https://dvdcst.dev/feed/rss.xml" rel="self" type="application/rss+xml" />
    <description>Writing about technology, work and whatever else crosses my path</description>
    <language>en</language>
    <item>
      <title>The read replica your code doesn&#39;t know about</title>
      <link>https://dvdcst.dev/blog/the-read-replica-your-code-doesn-t-know-about/</link>
      <description>&lt;p&gt;Every backend I have worked on eventually hits the same problem. There is one endpoint, usually a listing or a report, that reads a lot and returns a lot, and it slowly becomes the reason the database has a bad day. Someone opens the catalog page, the query goes through a few million rows, and every write happening at that moment waits in line behind it.&lt;/p&gt;
&lt;p&gt;The textbook answer is boring and correct: send those reads to a replica. You probably already have one, or you can create one in a few minutes on any managed database. The primary handles writes, the replica handles the heavy reads.&lt;/p&gt;
&lt;p&gt;The part the textbook skips is the annoying one: how do you route a specific request to the replica without passing a connection object through every method that might touch the database?&lt;/p&gt;
&lt;p&gt;I got this wrong a few times before I got it boring. This is the boring version, in .NET. It is smaller than the title suggests: it routes a whole request to one server, not each query, and that limitation is what keeps it clean.&lt;/p&gt;
&lt;h2 id=&quot;the-naive-fix&quot;&gt;The naive fix&lt;/h2&gt;
&lt;p&gt;The first instinct is to pass the connection down. You add a second &lt;code&gt;DbContext&lt;/code&gt;, a &lt;code&gt;ReplicaCatalogDbContext&lt;/code&gt;, and inject it into the one controller that needs it.&lt;/p&gt;
&lt;p&gt;That works for exactly one endpoint. Then the report calls a service, the service calls another service, and now you are passing a &amp;quot;which database&amp;quot; flag through five layers that had no business knowing a database exists. Every new read path becomes a decision. Every refactor drags the flag along.&lt;/p&gt;
&lt;p&gt;I have written that version. It looks fine in the pull request and it is annoying six months later.&lt;/p&gt;
&lt;p&gt;What I wanted was this: the service layer stays exactly as it is, and somewhere else, out of the way, something decides which connection this request uses. One place. One line per endpoint.&lt;/p&gt;
&lt;h2 id=&quot;context-not-a-parameter&quot;&gt;Context, not a parameter&lt;/h2&gt;
&lt;p&gt;&amp;quot;Which database does this request use&amp;quot; is not a parameter, it is context. It belongs to the request, not to any function inside it. So instead of passing it down, you put it somewhere the whole request can read, and you read it back when you open the connection.&lt;/p&gt;
&lt;p&gt;The simpler version of this uses a static &lt;code&gt;AsyncLocal&amp;lt;bool&amp;gt;&lt;/code&gt;. I prefer a scoped service: it already has a per request lifetime, it is easier to test, and it does not leak request state into work that outlives the request.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-csharp&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;enum&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;DatabaseTarget&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    Primary&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    Replica
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;sealed&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;RequestDbTarget&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token return-type class-name&quot;&gt;DatabaseTarget&lt;/span&gt; Target &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; DatabaseTarget&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Primary&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is the whole state, and it defaults to the primary so forgetting to decide is the safe option. Register it as scoped:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-csharp&quot;&gt;builder&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Services&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token generic-method&quot;&gt;&lt;span class=&quot;token function&quot;&gt;AddScoped&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;RequestDbTarget&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A note on the name. My first version called it &lt;code&gt;ReadOnly&lt;/code&gt;, which was a lie: nothing here makes anything read only, and the compiler will happily send a &lt;code&gt;SaveChangesAsync&lt;/code&gt; to a replica that rejects it at runtime. &lt;code&gt;DatabaseTarget.Replica&lt;/code&gt; says what it actually does, which is pick a server.&lt;/p&gt;
&lt;h2 id=&quot;flipping-the-target-without-touching-the-handler&quot;&gt;Flipping the target without touching the handler&lt;/h2&gt;
&lt;p&gt;I want to mark an endpoint the same way I mark any other cross-cutting concern: with an attribute. Nothing inside the action changes, I just tag it.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-csharp&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token attribute&quot;&gt;&lt;span class=&quot;token class-name&quot;&gt;AttributeUsage&lt;/span&gt;&lt;span class=&quot;token attribute-arguments&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;AttributeTargets&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Method &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; AttributeTargets&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Class&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;sealed&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;UseReplicaAttribute&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token type-list&quot;&gt;&lt;span class=&quot;token class-name&quot;&gt;Attribute&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The attribute does nothing on its own. A small middleware, placed right after routing so the endpoint is already resolved, is what reads it and sets the target:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-csharp&quot;&gt;app&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;UseRouting&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

app&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Use&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;context&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; next&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt;&lt;/span&gt; useReplica &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; context&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;GetEndpoint&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;?&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Metadata&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token generic-method&quot;&gt;&lt;span class=&quot;token function&quot;&gt;GetMetadata&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;UseReplicaAttribute&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;is&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;useReplica&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        context&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;RequestServices
            &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token generic-method&quot;&gt;&lt;span class=&quot;token function&quot;&gt;GetRequiredService&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;RequestDbTarget&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Target &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; DatabaseTarget&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Replica&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;next&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There is a requirement hiding here: this middleware has to run before anything resolves the &lt;code&gt;DbContext&lt;/code&gt;. Usually that is free, since the context is created when the controller asks for it and the controller runs after routing. But if some earlier middleware resolves &lt;code&gt;CatalogDbContext&lt;/code&gt; for its own reasons, the connection string is already chosen and your target arrives too late. The order is part of the design, not a detail.&lt;/p&gt;
&lt;h2 id=&quot;the-dbcontext-picks-a-side&quot;&gt;The DbContext picks a side&lt;/h2&gt;
&lt;p&gt;Two connection strings in configuration, nothing exotic:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;ConnectionStrings&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;Primary&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Server=primary;Database=Shop;...&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;Replica&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Server=replica;Database=Shop;...&quot;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the registration reads the target when it builds the options:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-csharp&quot;&gt;builder&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Services&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token generic-method&quot;&gt;&lt;span class=&quot;token function&quot;&gt;AddDbContext&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;CatalogDbContext&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;sp&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; options&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt;&lt;/span&gt; target &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; sp&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token generic-method&quot;&gt;&lt;span class=&quot;token function&quot;&gt;GetRequiredService&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;RequestDbTarget&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Target&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt;&lt;/span&gt; config &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; sp&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token generic-method&quot;&gt;&lt;span class=&quot;token function&quot;&gt;GetRequiredService&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;IConfiguration&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt;&lt;/span&gt; connectionString &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; target &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token return-type class-name&quot;&gt;DatabaseTarget&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Replica
        &lt;span class=&quot;token punctuation&quot;&gt;?&lt;/span&gt;&lt;/span&gt; config&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;GetConnectionString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Replica&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; config&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;GetConnectionString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Primary&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    options&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;UseSqlServer&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;connectionString&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is it. No branching inside a controller, no second context injected by hand, no flag travelling through the service layer. The decision lives in two places that already existed for infrastructure reasons.&lt;/p&gt;
&lt;p&gt;One note: this assumes the default EF Core lifetime, one context per request, connection chosen when the context is built. If you use &lt;code&gt;AddDbContextPool&lt;/code&gt; or an &lt;code&gt;IDbContextFactory&lt;/code&gt; the timing changes, because a pooled or factory built context does not necessarily read your scoped target when you expect it to.&lt;/p&gt;
&lt;h2 id=&quot;what-the-endpoint-looks-like-now&quot;&gt;What the endpoint looks like now&lt;/h2&gt;
&lt;p&gt;The heavy listing that used to bully the primary:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-csharp&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token attribute&quot;&gt;&lt;span class=&quot;token class-name&quot;&gt;UseReplica&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token attribute&quot;&gt;&lt;span class=&quot;token class-name&quot;&gt;HttpGet&lt;/span&gt;&lt;span class=&quot;token attribute-arguments&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;products&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token return-type class-name&quot;&gt;Task&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;IEnumerable&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;ProductDto&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;ListProducts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token attribute&quot;&gt;&lt;span class=&quot;token class-name&quot;&gt;FromQuery&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ProductFilter&lt;/span&gt; filter&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; _catalog&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Search&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;filter&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One attribute. Now picture the calls underneath:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ProductsController
   -&amp;gt; CatalogService
      -&amp;gt; InventoryService
         -&amp;gt; PriceService
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;None of those services knows a replica exists. &lt;code&gt;CatalogService.Search&lt;/code&gt; runs the same query it always ran, against a connection it never chose, and that connection now points at the replica. The write endpoints next to it keep hitting the primary, because they are not tagged.&lt;/p&gt;
&lt;p&gt;When a new report shows up next quarter, pointing it at the replica is a one line diff. Nobody has to open a service to do it.&lt;/p&gt;
&lt;h2 id=&quot;it-routes-a-request-not-an-operation&quot;&gt;It routes a request, not an operation&lt;/h2&gt;
&lt;p&gt;This is where the pattern stops being magic. The target is set once, for the whole request, before any database work happens. Every query in that request goes to the same server. Usually that is what you want. Sometimes it is not.&lt;/p&gt;
&lt;p&gt;Picture an endpoint that writes and then reads its own write:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-csharp&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token return-type class-name&quot;&gt;Task&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;IActionResult&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Save&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;ProductInput&lt;/span&gt; input&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; _catalog&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Update&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;input&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt;&lt;/span&gt; refreshed &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; _catalog&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;ListProducts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Ok&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;refreshed&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You cannot tag that one with &lt;code&gt;[UseReplica]&lt;/code&gt;. The update goes to the primary, and the read right after it also has to go to the primary, because the replica may not have caught up and would return the version from before you saved. That is not a read only request. It is a write request that happens to read afterwards, and both halves belong on the primary.&lt;/p&gt;
&lt;p&gt;So be honest about what this buys you. It has no answer for &amp;quot;write to the primary, then run a heavy report on the replica, in the same request&amp;quot;. When a request genuinely needs both, this is the wrong tool. Per operation routing exists, you pick a connection at each query, it just costs a lot more machinery and pushes the concern back into the code you were trying to keep clean. Most apps do not need it.&lt;/p&gt;
&lt;h2 id=&quot;replicas-lag&quot;&gt;Replicas lag&lt;/h2&gt;
&lt;p&gt;A replica is not a faster copy of the truth. It lags, and it lags in ways that matter.&lt;/p&gt;
&lt;p&gt;The classic trap is read after write across two requests. A user updates their shipping address, the browser immediately refetches the profile, and if that read went to the replica it may come back with the old address. The user thinks the save failed. It did not, you just asked the wrong server.&lt;/p&gt;
&lt;p&gt;The rule I settled on is simple: the replica is for reads that tolerate slightly stale data. Catalogs, dashboards, reports, search. Anything a user just wrote and expects to see back stays on the primary.&lt;/p&gt;
&lt;figure&gt;
    &lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;https://dvdcst.dev/blog/the-read-replica-your-code-doesn-t-know-about/CqThkXOVNb-309.avif 309w&quot;&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://dvdcst.dev/blog/the-read-replica-your-code-doesn-t-know-about/CqThkXOVNb-309.webp 309w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://dvdcst.dev/blog/the-read-replica-your-code-doesn-t-know-about/CqThkXOVNb-309.jpeg&quot; alt=&quot;How DBAs look at you when you use a simple JSON file instead of a sharded multi-region cluster&quot; width=&quot;309&quot; height=&quot;330&quot;&gt;&lt;/picture&gt;
  &lt;figcaption&gt;One replica, used on purpose, beats twelve you cannot explain (via &lt;a href=&quot;https://www.instagram.com/devhumour.ai/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;@devhumour.ai&lt;/a&gt;)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;A replica can also fall behind under load and it can fail over. None of this is a reason to avoid replicas, it is a reason to be deliberate about which endpoints you point at one. Which is exactly why I like that the decision is one visible word above a method instead of a default buried in a service.&lt;/p&gt;
&lt;h2 id=&quot;closing-thoughts&quot;&gt;Closing thoughts&lt;/h2&gt;
&lt;p&gt;This pattern hides in the two places that were already infrastructure. The middleware was going to exist. The &lt;code&gt;DbContext&lt;/code&gt; registration was going to exist. The service layer, the part that is actually mine to get right, never learns any of this is happening.&lt;/p&gt;
&lt;p&gt;That is the same idea I keep writing about from different angles: spend your cleverness once, in a corner, and be boring everywhere else. A read replica is about as boring as infrastructure gets, and switching an endpoint onto it now costs one word above a method. The 10PM version of me, debugging a slow query during an incident, will be glad the routing is in one obvious place and that its one limitation is written down instead of discovered.&lt;/p&gt;
</description>
      <pubDate>Sun, 26 Jul 2026 00:00:00 GMT</pubDate>
      <dc:creator>dvdcst</dc:creator>
      <guid>https://dvdcst.dev/blog/the-read-replica-your-code-doesn-t-know-about/</guid>
    </item>
    <item>
      <title>Boring Technology Wins</title>
      <link>https://dvdcst.dev/blog/boring-technology-wins/</link>
      <description>&lt;p&gt;Every few months some new tech trends:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A brand new database that will finally replace Postgres&lt;/li&gt;
&lt;li&gt;A language that will finally kill PHP&lt;/li&gt;
&lt;li&gt;A paradigm that makes everything you shipped last year look naive&lt;/li&gt;
&lt;/ul&gt;
&lt;figure&gt;
    &lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;https://dvdcst.dev/blog/boring-technology-wins/Z6pWAQ1JD7-500.avif 500w&quot;&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://dvdcst.dev/blog/boring-technology-wins/Z6pWAQ1JD7-500.webp 500w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://dvdcst.dev/blog/boring-technology-wins/Z6pWAQ1JD7-500.png&quot; alt=&quot;Standards are great. Let`s build a new one&quot; width=&quot;500&quot; height=&quot;283&quot;&gt;&lt;/picture&gt;
  &lt;figcaption&gt;The industry never runs out of &quot;one more standard&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Four years ago I would read the announcement, get excited, and start rewriting something that was working perfectly fine.&lt;/p&gt;
&lt;p&gt;These days I read the announcement, think &amp;quot;cool&amp;quot;, and go back to my Postgres, node and .NET. For a while I assumed this was the moment developers stop optimizing for novelty and start optimizing for reliability.&lt;/p&gt;
&lt;p&gt;It is not that. Boring is a feature, and as usual it took a few late-afternoon incidents to understand why.&lt;/p&gt;
&lt;h2 id=&quot;boring-does-not-mean-outdated&quot;&gt;Boring does not mean outdated&lt;/h2&gt;
&lt;p&gt;Boring technology is technology whose failure modes you already know. Postgres is boring not because it is old, but because when it breaks, it breaks in ways thousands of people have already documented on every imaginable place. Its behavior under load is known. Its sharp edges are mapped. The set of things that can genuinely surprise you is small, and most of the surprises that remain are one search away from someone else&#39;s postmortem.&lt;/p&gt;
&lt;p&gt;A shiny new database might be better on paper. But every property of it is an unknown to &lt;em&gt;you&lt;/em&gt;, and unknowns are not free. You pay for them later, usually at the worst possible time, usually with interest.&lt;/p&gt;
&lt;h2 id=&quot;the-cost-of-new-is-paid-later-by-a-tired-version-of-you&quot;&gt;The cost of new is paid later, by a tired version of you&lt;/h2&gt;
&lt;p&gt;In an &lt;a href=&quot;https://dvdcst.dev/blog/my-mental-model-for-writing-simpler-backend-systems/&quot;&gt;earlier post&lt;/a&gt; I mentioned that the 10PM version of me is always more tired and less patient than the 9AM one. Boring technology is mostly a gift to that person.&lt;/p&gt;
&lt;p&gt;When you reach for the exotic tool, you are making a bet that you, or whoever is on call, will still understand it in eighteen months.&lt;/p&gt;
&lt;figure&gt;
    &lt;img src=&quot;https://dvdcst.dev/blog/boring-technology-wins/WAzPAe7tk3-400.webp&quot; alt=&quot;When you realize you made a bad past choice&quot; width=&quot;400&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; height=&quot;452&quot;&gt;
  &lt;figcaption&gt;Some times you are just cooked&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;That the maintainer will still be around. That the three GitHub issues describing your exact problem will not be the only three that ever existed (and they never was solved). Every one of those bets is fine in isolation. The trouble is that they compound, and the bill always arrives during an incident, never during a calm Tuesday.&lt;/p&gt;
&lt;p&gt;Someone once framed this as a budget of innovation tokens: you get a small number, and every novel choice spends one. The mistake I made for years was spending them on infrastructure, the part of the system that is supposed to be invisible, instead of saving them for the part that was actually mine to solve.&lt;/p&gt;
&lt;h2 id=&quot;hype-optimizes-for-the-demo-not-for-tuesday&quot;&gt;Hype optimizes for the demo, not for Tuesday&lt;/h2&gt;
&lt;p&gt;A lot of technology looks incredible in a fancy conference talk. Five minutes, a clean dataset, a problem shaped exactly to fit the tool. That is the demo.&lt;/p&gt;
&lt;p&gt;Then you take it home and run it against real data, real concurrency, real users doing things no one designed for, and a deploy pipeline that has to keep working at 3AM. That is Tuesday. Most hype is tuned for the demo. Boring tech has already survived a few thousand Tuesdays, which is the only review that has ever mattered to me.&lt;/p&gt;
&lt;p&gt;This is also why I distrust benchmarks more than I distrust my own taste now. A benchmark is a demo with a chart. The question I actually care about is not &amp;quot;how fast is it when everything goes right&amp;quot;, it is &amp;quot;how badly does it hurt when something goes wrong&amp;quot;, and nobody puts that on a landing page.&lt;/p&gt;
&lt;h2 id=&quot;spend-your-novelty-where-it-actually-matters&quot;&gt;Spend your novelty where it actually matters&lt;/h2&gt;
&lt;p&gt;None of this is an argument for never trying anything. It is an argument for choosing &lt;em&gt;one&lt;/em&gt; place to be interesting and being aggressively dull everywhere else.&lt;/p&gt;
&lt;p&gt;When I built a daily movie guessing game a while back, the only genuinely novel part was the data generation concept: generating a full year of daily prompts with an LLM ahead of time and matching player guesses with some string-distance math. Everything around it was deliberately boring. A static frontend. No backend. No auth. Hosted on a reliable and safe infrastructure I already understood. The boring scaffolding is exactly what let me pour all of my attention into the one part that was worth the risk.&lt;/p&gt;
&lt;p&gt;That is the whole trick. Identify the single hard problem that is the reason your project exists, spend your novelty there, and let the rest of the stack be as unremarkable as possible. A system where everything is exciting is a system where everything can fail in a way you have never seen.&lt;/p&gt;
&lt;h2 id=&quot;closing-thoughts&quot;&gt;Closing thoughts&lt;/h2&gt;
&lt;p&gt;Choosing boring technology is not a sign that you have stopped caring or stopped learning. It is usually the opposite. It means you have been burned enough times to know that the goal was never to look clever in a code review.&lt;/p&gt;
&lt;p&gt;The goal is to still understand the system when you are tired, to fail in ways you can predict, and to spend your limited capacity for risk on the one problem that deserves it. Boring is what is left after you remove the parts that were only there to impress someone.&lt;/p&gt;
&lt;p&gt;Most of the time, that is the whole system.&lt;/p&gt;
</description>
      <pubDate>Fri, 29 May 2026 00:00:00 GMT</pubDate>
      <dc:creator>dvdcst</dc:creator>
      <guid>https://dvdcst.dev/blog/boring-technology-wins/</guid>
    </item>
    <item>
      <title>Knowing When to Let a Project Die</title>
      <link>https://dvdcst.dev/blog/knowing-when-to-let-a-project-die/</link>
      <description>&lt;p&gt;Every developer has a graveyard. Mine has a CHIP-8 emulator from a couple of years ago, half a college RPG written in C, a Gatsby portfolio (and a Next.js one, and a... you got it), some automation scripts that solved problems I no longer have, and a notebook full of side project ideas I will never start.&lt;/p&gt;
&lt;p&gt;For a long time I treated this as a personal flaw. Lack of discipline, lack of focus, lack of &lt;em&gt;something&lt;/em&gt;. Then I noticed that the developers I respect have graveyards too, sometimes bigger than mine. And the ones with the smallest graveyards were not the most productive ones, they were the most &lt;em&gt;boring&lt;/em&gt; ones.&lt;/p&gt;
&lt;p&gt;So the real question is not how to stop abandoning projects. It is knowing when abandoning is the correct move, and when it is just the easy one.&lt;/p&gt;
&lt;p&gt;After enough years doing this, I noticed my abandonments fall into four buckets.&lt;/p&gt;
&lt;h2 id=&quot;1-the-project-that-taught-you-and-stopped&quot;&gt;1. The project that taught you and stopped&lt;/h2&gt;
&lt;p&gt;This is the classic one. You start a CHIP-8 emulator because reading about how old systems worked feels exciting. The first week is amazing, opcodes mapped, display rendering, the test ROM finally showing something. Then you hit the part that is just... typing. The drawing logic, the input handling, the edge cases of the BCD instruction.&lt;/p&gt;
&lt;p&gt;The dopamine drops. The IDE stays closed.&lt;/p&gt;
&lt;p&gt;I have a folder for this kind of project. F0ReST, the text RPG from college that ended its life with a single bear fight and an ASCII title screen. The CHIP-8 I keep promising myself I will go back to. A Lex/YACC compiler I wrote for the same class and never extended.&lt;/p&gt;
&lt;p&gt;The honest truth: most of these &lt;em&gt;should&lt;/em&gt; be abandoned. Their purpose was learning, and the learning happened in week one. Finishing them would teach me nothing new, just punish me for getting excited in the first place.&lt;/p&gt;
&lt;figure&gt;
    &lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;https://dvdcst.dev/blog/knowing-when-to-let-a-project-die/Ydv0QV9YWh-400.avif 400w&quot;&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://dvdcst.dev/blog/knowing-when-to-let-a-project-die/Ydv0QV9YWh-400.webp 400w&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://dvdcst.dev/blog/knowing-when-to-let-a-project-die/Ydv0QV9YWh-400.png&quot; alt=&quot;Silence, current side project. A new side project is talking&quot; width=&quot;400&quot; height=&quot;475&quot;&gt;&lt;/picture&gt;
&lt;/figure&gt;
&lt;p&gt;The trap is feeling guilty about it. Guilt makes you start the same kind of project next month, because deep down you think &lt;em&gt;this time&lt;/em&gt; you will be different. You will not. Accept the pattern, archive the repo, move on.&lt;/p&gt;
&lt;h2 id=&quot;2-the-project-that-life-outgrew&quot;&gt;2. The project that life outgrew&lt;/h2&gt;
&lt;p&gt;This one hurts more, because the project was good and you were enjoying it.&lt;/p&gt;
&lt;p&gt;A while ago I jumped into Omarchy, an Arch based distro built around a tiling window manager. The first week was a series of small battles: Wi-Fi through a TUI whose keybindings I had to look up on GitHub, a Brazilian keyboard layout that broke the installer, workspaces in a world where minimizing a window is not a concept. I had a good time anyway. The system was fast, the workflow was interesting, my desktop looked like a screenshot from &lt;a href=&quot;https://www.reddit.com/r/unixporn/&quot; class=&quot;external-link&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;r/unixporn&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I am not on Omarchy anymore. I am on Fedora.&lt;/p&gt;
&lt;p&gt;Nothing about Omarchy got worse. I got busier. Personal life, hobbies that do not involve a screen, friends, family. At some point I noticed I was spending Saturday afternoons tweaking waybar instead of doing the things I had actually been wanting to do. The operating system became the project, and the project was competing with my life.&lt;/p&gt;
&lt;p&gt;Fedora boots, runs my stack, and asks for nothing back. That is exactly what I needed.&lt;/p&gt;
&lt;p&gt;This kind of abandonment is different because the project was not the problem. The problem was the position it occupied in my week. Sometimes the move is not to finish or quit, it is to demote.&lt;/p&gt;
&lt;h2 id=&quot;3-the-project-built-to-die&quot;&gt;3. The project built to die&lt;/h2&gt;
&lt;p&gt;A while back I built a small daily browser game: three emojis and a couple of cryptic clues, and you had to guess the movie. It was supposed to die, and it did.&lt;/p&gt;
&lt;p&gt;I knew up front that it would be online for one year. Twelve months of daily challenges, then dark. That decision changed everything about how I built it: no auth, no backend, no scale considerations, no roadmap. A static frontend, some clever string distance for typos, and a year of prompts generated with an LLM.&lt;/p&gt;
&lt;p&gt;It ran for the full year and then I pulled the plug. The numbers were exactly what I expected on one side and slightly unsettling on the other: a small but steady stream of players from week one to week 52, and a roughly equal stream of bots and would-be hackers prodding the infrastructure the entire time. Two flat lines on the same chart, one of users, one of attackers, oddly similar in shape. A small reminder that anything you put on the public internet is interesting to &lt;em&gt;someone&lt;/em&gt;, even if the someone is a script.&lt;/p&gt;
&lt;p&gt;This is the most underrated way to &amp;quot;abandon&amp;quot; something. You decide at the start that the project has an expiration date, and that frees you to make it exactly as small as it needs to be. No premature scaling. No &amp;quot;what if it goes viral&amp;quot;. No five year vision document.&lt;/p&gt;
&lt;p&gt;It also makes the eventual shutdown emotionally trivial. You are not abandoning anything, you are collecting on a debt the project owed you from day one.&lt;/p&gt;
&lt;h2 id=&quot;4-the-project-that-knows-when-it-is-done&quot;&gt;4. The project that knows when it is done&lt;/h2&gt;
&lt;p&gt;The other side of the coin.&lt;/p&gt;
&lt;p&gt;Right now I am building a small endless runner. Nothing ambitious, a single mechanic, minimal art, maybe a leaderboard if I feel generous. What makes it different from most of my graveyard projects is not a deadline, it is the &lt;em&gt;definition&lt;/em&gt;. Before I wrote a line of code I knew exactly what &amp;quot;done&amp;quot; looked like for v1. Whether it eventually ships to a store, sits on itch.io, or just lives on my hard drive is a separate decision and frankly a smaller one.&lt;/p&gt;
&lt;p&gt;That distinction matters more than I used to think. A deadline is about &lt;em&gt;when&lt;/em&gt; you stop. A closed scope is about &lt;em&gt;what&lt;/em&gt; counts as stopping. Most of my graveyard had neither.&lt;/p&gt;
&lt;p&gt;Writing down what &amp;quot;done&amp;quot; means before the first commit sounds boring, and it is. It is also the cheapest insurance I have found against scope creep, against losing interest halfway through, against the project quietly mutating into something I no longer want to finish. The plan can still change, but at least there is one to change.&lt;/p&gt;
&lt;h2 id=&quot;how-i-decide-now&quot;&gt;How I decide now&lt;/h2&gt;
&lt;p&gt;When I feel the urge to walk away from something, I run a short list before I close the tab:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Did I get what I came for?&lt;/strong&gt; If the project was learning, did I learn the thing? If yes, abandoning is fine. The repo is just a receipt.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Is the next 20% going to teach me anything?&lt;/strong&gt; If the rest of the work is mechanical execution of stuff I already know, the project is done in my head. The code just has not caught up.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Am I quitting because it is hard, or because it is not mine anymore?&lt;/strong&gt; Hard is a reason to push through. Not mine anymore, when my life has shifted and the project belongs to a previous version of me, is a reason to let it go.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Did I promise anyone, including myself, that this would ship?&lt;/strong&gt; If yes, finish it. Even a small public commitment matters more than most people admit.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Most of the time the answer is &amp;quot;yes, abandon it&amp;quot;, and the guilt is what makes me hesitate, not the project itself.&lt;/p&gt;
&lt;h2 id=&quot;closing-thoughts&quot;&gt;Closing thoughts&lt;/h2&gt;
&lt;p&gt;The graveyard is not the problem. The problem is treating every project like it has to live forever, and then feeling like a failure when something does not.&lt;/p&gt;
&lt;p&gt;Some projects are lessons. Some are built to die. Some are demoted because life got bigger. And every once in a while, one is built knowing exactly what done looks like.&lt;/p&gt;
&lt;p&gt;All four are fine. The trick is knowing which one you are holding.&lt;/p&gt;
</description>
      <pubDate>Wed, 27 May 2026 00:00:00 GMT</pubDate>
      <dc:creator>dvdcst</dc:creator>
      <guid>https://dvdcst.dev/blog/knowing-when-to-let-a-project-die/</guid>
    </item>
    <item>
      <title>My Mental Model for Writing Simpler Backend Systems</title>
      <link>https://dvdcst.dev/blog/my-mental-model-for-writing-simpler-backend-systems/</link>
      <description>&lt;p&gt;After enough APIs built, legacy systems refactored, and late night production incidents, something started to become obvious: backend engineering is not about frameworks. It is about reducing chaos.&lt;/p&gt;
&lt;p&gt;Which caos you ask?&lt;/p&gt;
&lt;p&gt;I know, this sounds like a goofy tech coach quote, but is the reality. The client doesn&#39;t care about our event based systems, CQRS and AWS, they want a report when they click the blue button, a cheaper infra and the most specific possible business rules on production.&lt;/p&gt;
&lt;p&gt;That creeping disorder even has a name: code entropy the natural tendency of systems to drift toward complexity and disorder as they grow, requirements shift, and teams change.&lt;/p&gt;
&lt;p&gt;Reducing the said chaos is not easy, and to aid myself i have this is the mental model I try to follow when I write code. It&#39;s not perfect. It&#39;s not finished. It&#39;s what I aim for.&lt;/p&gt;
&lt;h2 id=&quot;1-my-job-is-to-reduce-chaos&quot;&gt;1. My job is to reduce chaos&lt;/h2&gt;
&lt;p&gt;Systems grow, rules change, people leave. If the system becomes harder to reason about over time, I failed. Backend exists to create boundaries, clear ones. And that&#39;s it. Computers are designed to be predictable machines, and our code should be designed with this on mind.&lt;/p&gt;
&lt;h2 id=&quot;2-business-rules-are-sacred&quot;&gt;2. Business rules are sacred&lt;/h2&gt;
&lt;p&gt;Frameworks change, cloud providers change, databases change. Business rules stay. I try to keep domain logic isolated, avoid coupling rules to transport (HTTP, queues), and test rules independently from infrastructure. Infrastructure serves the domain, never the opposite.&lt;/p&gt;
&lt;p&gt;This sounds obvious until you&#39;re three sprints deep into a codebase where the business logic lives inside a controller.&lt;/p&gt;
&lt;h2 id=&quot;3-clarity-beats-cleverness&quot;&gt;3. Clarity beats cleverness&lt;/h2&gt;
&lt;p&gt;If I need to explain it twice, it&#39;s too complex. I prefer explicit names, small functions, predictable flow, and less magic. Code should feel boring and stable. The kind of code you open six months later and immediately understand. That&#39;s the goal, like a old school COBOL system.&lt;/p&gt;
&lt;h2 id=&quot;4-design-for-today-evolve-when-it-hurts&quot;&gt;4. Design for today, evolve when it hurts&lt;/h2&gt;
&lt;p&gt;I don&#39;t optimize for imaginary scale. I solve the current problem well, then I observe. Premature complexity is just ego disguised as foresight. We&#39;ve all done it, it&#39;s never worth it.&lt;/p&gt;
&lt;h2 id=&quot;5-failure-is-expected&quot;&gt;5. Failure is expected&lt;/h2&gt;
&lt;p&gt;Networks fail, dependencies fail, people make mistakes. Good backend systems fail predictably, log with context, avoid corrupting state, and prefer idempotent operations when possible. Resilience is intentional. You don&#39;t stumble into it.&lt;/p&gt;
&lt;h2 id=&quot;6-decisions-must-be-reversible-when-possible&quot;&gt;6. Decisions must be reversible when possible&lt;/h2&gt;
&lt;p&gt;Not every decision needs a committee. If it&#39;s reversible, I decide. If it&#39;s expensive, I document and chat with my Lead. Progress beats paralysis, we do async work. And most decisions are more reversible than they feel in the moment.&lt;/p&gt;
&lt;h2 id=&quot;7-production-is-the-final-judge&quot;&gt;7. Production is the final judge&lt;/h2&gt;
&lt;p&gt;Opinions don&#39;t matter more than logs, metrics, and real user behavior. If it&#39;s not observable, it&#39;s not reliable. No matter how elegant the architecture looks on a whiteboard.&lt;/p&gt;
&lt;h2 id=&quot;8-simplicity-is-responsibility&quot;&gt;8. Simplicity is responsibility&lt;/h2&gt;
&lt;p&gt;The goal is not to impress. The goal is to make the system understandable for the next engineer, including future me. And the 10PM future me is always more tired and less patient than present 9AM me.&lt;/p&gt;
&lt;h2 id=&quot;closing-thoughts&quot;&gt;Closing thoughts&lt;/h2&gt;
&lt;p&gt;This is not rocket science. Backend is not about being brilliant. It&#39;s about building systems that survive change, entropy, and people. Do good work. That&#39;s the standard.&lt;/p&gt;
</description>
      <pubDate>Thu, 05 Mar 2026 00:00:00 GMT</pubDate>
      <dc:creator>dvdcst</dc:creator>
      <guid>https://dvdcst.dev/blog/my-mental-model-for-writing-simpler-backend-systems/</guid>
    </item>
    <item>
      <title>Local Backup with rsync and bash</title>
      <link>https://dvdcst.dev/blog/local-backup-with-rsync-and-bash/</link>
      <description>&lt;h2 id=&quot;introduction-15-gigabytes-is-not-enough&quot;&gt;Introduction: 15 gigabytes is not enough&lt;/h2&gt;
&lt;p&gt;Not having all the projects and assignments I did in college neatly organized in a single directory is one of my biggest regrets as someone who spends most of his life in front of a computer. Whether it was because of the infinite Google Drive storage my university provided, or the countless flash drives, emails, and random files I sent and received through every possible communication channel during those five years.&lt;/p&gt;
&lt;p&gt;As with all good things, I knew the infinite drive wouldn’t last forever, and as soon as I graduated, I transferred everything to my personal Google Drive.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Big mistake&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In one second, 99% of my free tier storage was gone, filled with messy, duplicated, and poorly formatted files, with the bonus that not everything had actually been transferred.
The solution: throw everything onto my computer and pray the hard drive doesn’t melt.&lt;/p&gt;
&lt;p&gt;For about two years, it actually didn’t melt, and the data stayed there, but the paranoia of losing everything started to take up a few bytes of my mental hard drive.&lt;/p&gt;
&lt;p&gt;Until last week...&lt;/p&gt;
&lt;h2 id=&quot;hardware-recycling&quot;&gt;Hardware recycling&lt;/h2&gt;
&lt;p&gt;Eventually, a 256 GB SSD from an old PC landed on my desk. It was the perfect excuse to finally bring some order to the chaos and, at the same time, stop depending on Google Drive.&lt;/p&gt;
&lt;p&gt;With a cheap external case, I turned that loose SSD into an external drive, and with a bit of bash, decided to write my own simple, straightforward local backup script. And to make it even better, I decided not only to back up my college files but also my code repositories and my Obsidian vault, my semi-second brain.&lt;/p&gt;
&lt;h2 id=&quot;rsync-and-paranoia&quot;&gt;Rsync and paranoia&lt;/h2&gt;
&lt;p&gt;My idea was to have two mirrors:&lt;/p&gt;
&lt;p&gt;From PC to SSD, the main backup.
From SSD to PC, a local copy, because redundancy is never too much.&lt;/p&gt;
&lt;p&gt;This way, even if the SSD dies, I still have a version on my computer, and vice versa.&lt;/p&gt;
&lt;p&gt;Without reinventing the wheel, I chose Rsync, the command-line utility for syncing and transferring files between locations.&lt;/p&gt;
&lt;h3 id=&quot;the-script&quot;&gt;The script&lt;/h3&gt;
&lt;p&gt;The script below mounts the SSD, if it isn’t already, synchronizes the configured directories, and generates logs for each run. Simple, direct, and efficient.&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token shebang important&quot;&gt;#!/bin/bash&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# === Configurations ===&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;SSD_MOUNT&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;/run/media/SSD&quot;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# the directory where I mount the SSD&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;SSD_UUID&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;062C43D82C43C303&quot;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# obtained via lsblk -f&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;PC_BASE&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;/home/me&quot;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# my home folder&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;BACKUP_DIR&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$PC_BASE&lt;/span&gt;/Backup&quot;&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;LOGFILE&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$PC_BASE&lt;/span&gt;/sync-backup.log&quot;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# === Functions ===&lt;/span&gt;
&lt;span class=&quot;token function-name function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;[&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;date&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;+%Y-%m-%d %H:%M:%S&#39;&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt;] &lt;span class=&quot;token variable&quot;&gt;$1&lt;/span&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;tee&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-a&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$LOGFILE&lt;/span&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token function-name function&quot;&gt;check_or_mount&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; mountpoint &lt;span class=&quot;token parameter variable&quot;&gt;-q&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$SSD_MOUNT&lt;/span&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;then&lt;/span&gt;
        log &lt;span class=&quot;token string&quot;&gt;&quot;SSD already mounted at &lt;span class=&quot;token variable&quot;&gt;$SSD_MOUNT&lt;/span&gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt;
        log &lt;span class=&quot;token string&quot;&gt;&quot;SSD not mounted, attempting to mount...&quot;&lt;/span&gt;
        &lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;mount&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$SSD_UUID&lt;/span&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$SSD_MOUNT&lt;/span&gt;&quot;&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; mountpoint &lt;span class=&quot;token parameter variable&quot;&gt;-q&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$SSD_MOUNT&lt;/span&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;then&lt;/span&gt;
            log &lt;span class=&quot;token string&quot;&gt;&quot;SSD mounted successfully.&quot;&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt;
            log &lt;span class=&quot;token string&quot;&gt;&quot;Failed to mount SSD at &lt;span class=&quot;token variable&quot;&gt;$SSD_MOUNT&lt;/span&gt;, aborting.&quot;&lt;/span&gt;
            &lt;span class=&quot;token builtin class-name&quot;&gt;exit&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;fi&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;fi&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# === Execution ===&lt;/span&gt;
log &lt;span class=&quot;token string&quot;&gt;&quot;=== Starting sync ===&quot;&lt;/span&gt;
check_or_mount

log &lt;span class=&quot;token string&quot;&gt;&quot;PC → SSD (Repositories)&quot;&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;rsync&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-avh&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--delete&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--exclude&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;node_modules&#39;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$PC_BASE&lt;/span&gt;/Work/Repositories/&quot;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$SSD_MOUNT&lt;/span&gt;/Repositories/&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;tee&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-a&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$LOGFILE&lt;/span&gt;&quot;&lt;/span&gt;

log &lt;span class=&quot;token string&quot;&gt;&quot;PC → SSD (Obsidian)&quot;&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;rsync&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-avh&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--delete&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$PC_BASE&lt;/span&gt;/Work/Obsidian/&quot;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$SSD_MOUNT&lt;/span&gt;/Obsidian/&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;tee&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-a&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$LOGFILE&lt;/span&gt;&quot;&lt;/span&gt;

log &lt;span class=&quot;token string&quot;&gt;&quot;SSD → PC (Local backup)&quot;&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;rsync&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-avh&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--delete&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$SSD_MOUNT&lt;/span&gt;/&quot;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$BACKUP_DIR&lt;/span&gt;/&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;tee&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-a&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$LOGFILE&lt;/span&gt;&quot;&lt;/span&gt;

log &lt;span class=&quot;token string&quot;&gt;&quot;=== Sync completed ===&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Running the script is simple, just grant execute permission.&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;chmod&lt;/span&gt; +x local-sync.sh &lt;span class=&quot;token comment&quot;&gt;# or whatever name you gave it&lt;/span&gt;
./local-sync.sh&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;the-small-details&quot;&gt;The small details&lt;/h2&gt;
&lt;p&gt;Fixed UUID, avoids headaches when the SSD changes its mount path.
Logs, each execution is recorded, so you know what got updated.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;--exclude &#39;node_modules&#39;&lt;/code&gt;, saves time and space.
&lt;code&gt;--delete&lt;/code&gt;, keeps mirrors truly in sync, but use with caution.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;To run automatically, you can add this script to cron and let the system handle the rest.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;conclusion-goodbye-infinite-drive&quot;&gt;Conclusion: goodbye infinite drive&lt;/h2&gt;
&lt;p&gt;With a small SSD and a 40-line script, I built a local backup routine, fast and 100% under my control.
No subscriptions, no slow uploads, and no dependency on any big tech goodwill.&lt;/p&gt;
&lt;p&gt;Sometimes progress is just deciding to take care of your own chaos, but I still miss that infinite drive, maybe I’ll enroll in a new college somewhere again...&lt;/p&gt;
</description>
      <pubDate>Tue, 07 Oct 2025 00:00:00 GMT</pubDate>
      <dc:creator>dvdcst</dc:creator>
      <guid>https://dvdcst.dev/blog/local-backup-with-rsync-and-bash/</guid>
    </item>
  </channel>
</rss>