<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Vincent Nyanga]]></title><description><![CDATA[Architecture lessons from building real systems — any stack, any industry]]></description><link>https://vincenyanga.me</link><image><url>https://substackcdn.com/image/fetch/$s_!277r!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fddb398ba-9934-4156-91cc-d23366d9b858_527x527.png</url><title>Vincent Nyanga</title><link>https://vincenyanga.me</link></image><generator>Substack</generator><lastBuildDate>Sun, 31 May 2026 08:46:35 GMT</lastBuildDate><atom:link href="https://vincenyanga.me/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Vincent Nyanga]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[vincentnyanga@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[vincentnyanga@substack.com]]></itunes:email><itunes:name><![CDATA[Vincent Nyanga]]></itunes:name></itunes:owner><itunes:author><![CDATA[Vincent Nyanga]]></itunes:author><googleplay:owner><![CDATA[vincentnyanga@substack.com]]></googleplay:owner><googleplay:email><![CDATA[vincentnyanga@substack.com]]></googleplay:email><googleplay:author><![CDATA[Vincent Nyanga]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Global Error Handling with Problem Details (RFC 9457) in ASP.NET Core]]></title><description><![CDATA[Every API returns errors.]]></description><link>https://vincenyanga.me/p/global-error-handling-with-problem</link><guid isPermaLink="false">https://vincenyanga.me/p/global-error-handling-with-problem</guid><dc:creator><![CDATA[Vincent Nyanga]]></dc:creator><pubDate>Fri, 29 May 2026 08:00:39 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!277r!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fddb398ba-9934-4156-91cc-d23366d9b858_527x527.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Every API returns errors. The question is whether those errors are useful to the consumer or just a generic &#8220;something went wrong&#8221; message that forces them to guess what happened.</p><p>If your API returns { &#8220;error&#8221;: &#8220;An error occurred&#8221; } or, worse, a raw exception stack trace, your consumers are working harder than they should. There is a standard for this, and ASP.NET Core supports it out of the box.</p><p>In this post, I will show you how to implement consistent, machine-readable error responses using Problem Details (RFC 9457) and the `IExceptionHandler` interface.</p><h2>What Is Problem Details?</h2><p>Problem Details is an RFC standard (RFC 9457, which replaced RFC 7807) that defines a consistent format for HTTP API error responses. The content type is `application/problem+json`, and every error includes these fields:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;json&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-json">{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.5",
  "title": "Not Found",
  "status": 404,
  "detail": "Article with ID '3fa85f64' was not found.",
  "instance": "/api/articles/3fa85f64"
}</code></pre></div><ul><li><p><strong>type</strong>: A URI reference that identifies the problem type</p></li><li><p><strong>title</strong>: A short, human-readable summary</p></li><li><p><strong>status</strong>: The HTTP status code</p></li><li><p><strong>detail</strong>: A human-readable explanation specific to this occurrence</p></li><li><p><strong>instance</strong>: A URI reference identifying this specific occurrence</p></li></ul><p>The format is extensible. You can add custom properties for additional context, like validation errors or trace IDs.</p><h2>Setting Up in ASP.NET Core</h2><p>The setup requires just three lines in your Program.cs:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;csharp&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-csharp">builder.Services.AddProblemDetails();

builder.Services.AddExceptionHandler&lt;GlobalExceptionHandler&gt;();

var app = builder.Build();

app.UseExceptionHandler();</code></pre></div><p><em>AddProblemDetails()</em> configures the framework to emit Problem Details responses from built-in middleware. <em>AddExceptionHandler&lt;T&gt;()</em> registers your custom exception handler. <em>UseExceptionHandler()</em> activates the middleware early in the pipeline.</p><h2>Writing an Exception Handler</h2><p>The <em>IExceptionHandler</em> interface has a single method: <em>TryHandleAsync</em>. It receives the <em>HttpContext</em> and the <em>Exception</em>, and returns true if it handled the exception or false to pass it to the next handler.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;csharp&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-csharp">public class GlobalExceptionHandler : IExceptionHandler
{

  private readonly ILogger&lt;GlobalExceptionHandler&gt; _logger;

  public GlobalExceptionHandler(ILogger&lt;GlobalExceptionHandler&gt; logger)
  {
    _logger = logger;
  }

  public async ValueTask&lt;bool&gt; TryHandleAsync(
    HttpContext httpContext,
    Exception exception,
    CancellationToken cancellationToken)
  {
    _logger.LogError(exception, &#8220;An unhandled exception occurred&#8221;);

    var (statusCode, title) = exception switch
    {
      NotFoundException =&gt; (StatusCodes.Status404NotFound, &#8220;Not Found&#8221;),
      ValidationException =&gt; (StatusCodes.Status400BadRequest, &#8220;Validation Error&#8221;),
      UnauthorizedAccessException =&gt; (StatusCodes.Status403Forbidden, &#8220;Forbidden&#8221;),
      _ =&gt; (StatusCodes.Status500InternalServerError, &#8220;Internal Server Error&#8221;)
    };

    var problemDetails = new ProblemDetails
      {
        Status = statusCode,
        Title = title,
        Detail = exception.Message,
        Instance = httpContext.Request.Path
      };

    httpContext.Response.StatusCode = statusCode;
    await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken);
    return true;
  }
}</code></pre></div><h2>Handler Chaining</h2><p>One of the best features of <em>IExceptionHandler</em> is that you can register multiple handlers. They are called in the order they were registered, and the first handler that returns true wins.</p><p>This lets you write focused handlers for specific exception types:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;csharp&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-csharp">// Handles validation exceptions specifically

public class ValidationExceptionHandler : IExceptionHandler
{
  public async ValueTask&lt;bool&gt; TryHandleAsync(
    HttpContext httpContext,
    Exception exception,
    CancellationToken cancellationToken)

  {
    if (exception is not ValidationException validationException)
      return false;

    var problemDetails = new ValidationProblemDetails(
        validationException.Errors
          .GroupBy(e =&gt; e.PropertyName)
          .ToDictionary(g =&gt; g.Key, g =&gt; g.Select(e =&gt; e.ErrorMessage).ToArray()))
    {
      Status = StatusCodes.Status400BadRequest,
      Title = &#8220;Validation Error&#8221;,
      Instance = httpContext.Request.Path
    };

    httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
    await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken);
    return true;
  }
}</code></pre></div><p>Register them in order of specificity:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;csharp&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-csharp">builder.Services.AddExceptionHandler&lt;ValidationExceptionHandler&gt;();
builder.Services.AddExceptionHandler&lt;NotFoundExceptionHandler&gt;();
builder.Services.AddExceptionHandler&lt;GlobalExceptionHandler&gt;(); // fallback</code></pre></div><p>The validation handler checks if the exception is a <em>ValidationException</em>. If not, it returns false, and the next handler gets a chance. The global handler at the end catches everything else.</p><h2>The Evolution of Error Handling in ASP.NET Core</h2><p>It is worth understanding how we got here, because you will see all of these approaches in existing codebases:</p><p><strong>1. Try-Catch in Every Controller (Don&#8217;t Do This)</strong></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;csharp&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-csharp">[HttpGet("{id}")]
public async Task&lt;IActionResult&gt; GetArticle(Guid id)
{
  try
  {
    var article = await _repository.GetByIdAsync(id);
    return Ok(article);
  }
  catch (NotFoundException)
  {
    return NotFound();
  }
  catch (Exception ex)
  {
    _logger.LogError(ex, "Error getting article");
    return StatusCode(500);
  }
}</code></pre></div><p>This duplicates error handling across every action method and produces inconsistent error responses.</p><p><strong>2. Exception Handling Middleware (Manual)</strong></p><p>A custom middleware that wraps the pipeline in a try-catch. Better than per-controller handling, but you end up writing the plumbing yourself.</p><p><strong>3. UseExceptionHandler with Lambda</strong></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;csharp&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-csharp">app.UseExceptionHandler(errorApp =&gt;
{
  errorApp.Run(async context =&gt;
  {
    context.Response.StatusCode = 500;
    await context.Response.WriteAsJsonAsync(
    new ProblemDetails { Title = &#8220;Error&#8221; });
  });
});</code></pre></div><p>Functional, but limited. No access to the exception type for differentiated responses.</p><p><strong>4. IExceptionHandler (.NET 8+, Recommended)</strong></p><p>The current best practice. Structured, chainable, testable, and fully integrated with the Problem Details framework.</p><h2><strong> .</strong>NET 9+: StatusCodeSelector</h2><p>.NET 9 introduced <em>StatusCodeSelector</em>, which simplifies common exception-to-status-code mappings:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;csharp&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-csharp">builder.Services.AddProblemDetails();
app.UseExceptionHandler(new ExceptionHandlerOptions
{
    StatusCodeSelector = ex =&gt; ex switch
    {
        ArgumentException =&gt; StatusCodes.Status400BadRequest,
        UnauthorizedAccessException =&gt; StatusCodes.Status401Unauthorized,
        _ =&gt; StatusCodes.Status500InternalServerError
    }
});</code></pre></div><p>For straightforward mappings where you do not need custom logic, this significantly reduces boilerplate.</p><h2>Adding Custom Extensions</h2><p>Problem Details is extensible. Add a trace ID, error code, or any other context your consumers need:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;csharp&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-csharp">var problemDetails = new ProblemDetails
{
  Status = statusCode,
  Title = title,
  Detail = exception.Message
};

problemDetails.Extensions["traceId"] =
Activity.Current?.Id ?? httpContext.TraceIdentifier;
problemDetails.Extensions["errorCode"] = "ARTICLE_NOT_FOUND";</code></pre></div><p>This produces:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;json&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-json">{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.5",
"title": "Not Found",
"status": 404,
"detail": "Article with ID '3fa85f64' was not found.",
"traceId": "00-abc123-def456-01",
"errorCode": "ARTICLE_NOT_FOUND"
}</code></pre></div><h2>Conclusion</h2><p>Consistent error responses are not optional for a professional API. Problem Details provides a standard format your consumers can rely on, and IExceptionHandler offers a clean, testable way to produce those responses.</p><p>The setup is minimal: register Problem Details, write your exception handlers, and activate the middleware. From that point on, every error your API returns follows the same structure: the correct status code, a useful message, and any additional context the consumer needs.</p><p>Stop inventing error formats. Use the standard. Your API consumers will thank you.</p>]]></content:encoded></item><item><title><![CDATA[Database Sharding]]></title><description><![CDATA[Splitting Your Data Without Splitting Your Sanity]]></description><link>https://vincenyanga.me/p/database-sharding</link><guid isPermaLink="false">https://vincenyanga.me/p/database-sharding</guid><dc:creator><![CDATA[Vincent Nyanga]]></dc:creator><pubDate>Fri, 15 May 2026 08:02:02 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!0rPc!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1541a1b4-2e87-4d25-a79d-6cc355d681fe_1200x1500.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Introduction</h2><p>Imagine you run a library. At first, one building holds all the books, and a single catalogue helps visitors find what they need. But the library grows. The shelves overflow, the aisles are packed, and the catalogue desk has a line out the door. You have two choices: build a bigger building (vertical scaling) or open multiple branches across the city and distribute the collection (horizontal scaling).</p><p>Database sharding is the second option. It&#8217;s the practice of splitting your data across multiple database instances, called <strong>shards</strong>, so that no single server has to bear the entire load. In this post, we&#8217;ll explore how sharding works, the strategies you can use, how to pick the right shard key, and the real-world pain of cross-shard queries. By the end, you&#8217;ll have a solid understanding of when sharding makes sense and how companies like Instagram, Discord, and Slack have implemented it at scale.</p><p>Let&#8217;s get started!</p><h2>What Is Database Sharding?</h2><p>Sharding is a form of <strong>horizontal partitioning</strong> where rows of a database table are distributed across multiple independent database instances. Each shard holds a subset of the data, and together they represent the complete dataset.</p><p>Unlike read replicas (which duplicate the entire dataset for read scaling), sharding splits the data itself. This means both reads &#120354;&#120367;&#120357; writes scale horizontally, something replicas alone cannot achieve.</p><p>Going back to our library analogy: read replicas are like printing extra copies of the catalogue so more people can look things up at the same time. Sharding is like distributing the actual books across multiple branches, so no single building runs out of shelf space.</p><h2>Sharding Strategies</h2><p>There are three primary strategies for deciding which shard holds which data. Each comes with trade-offs.</p><h3>Range-Based Sharding</h3><p>Data is partitioned based on continuous ranges of the shard key. For example, user IDs 1 to 1,000,000 go to Shard A, 1,000,001 to 2,000,000 go to Shard B, and so on.</p><p><strong>Advantages:</strong></p><ul><li><p>Keeps adjacent data together, making range scans efficient</p></li><li><p>Simple to implement and easy to reason about</p></li></ul><p><strong>Disadvantages:</strong></p><ul><li><p>Creates hotspots when new data clusters at one end of the range (the shard holding the newest data gets hammered with writes)</p></li><li><p>Leads to unbalanced shards over time</p></li></ul><p><strong>Best for:</strong> Time-series data where range queries are the dominant access pattern.</p><h3>Hash-Based Sharding</h3><p>A hash function is applied to the shard key, and the result determines which shard stores the record:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">shard_number = hash(user_id) % number_of_shards</code></pre></div><p><strong>Advantages:</strong></p><ul><li><p>Distributes data and traffic most evenly across shards</p></li><li><p>Dramatically reduces hotspot risk</p></li></ul><p><strong>Disadvantages:</strong></p><ul><li><p>Range queries become expensive; fetching user IDs 2M to 3M may scatter across hundreds of shards, forcing scatter-gather operations</p></li><li><p>Adding or removing shards changes the modulo value, meaning most keys need rehashing</p></li></ul><p>This is the most common general-purpose strategy. To mitigate the rehashing problem, many systems use <strong>consistent hashing</strong>, where data and servers are placed on a virtual ring. Adding a node only requires redistributing roughly 1/N of the data, rather than rehashing everything.</p><p><strong>Best for:</strong> High-write workloads with primarily point lookups (get by ID).</p><h3>Directory-Based (Lookup) Sharding</h3><p>A centralised lookup table maps shard keys to specific shards. Every query first consults this directory to find the target shard.</p><p><strong>Advantages:</strong></p><ul><li><p>Maximum flexibility: move users between shards without changing application logic</p></li><li><p>Isolate high-traffic tenants onto dedicated shards</p></li></ul><p><strong>Disadvantages:</strong></p><ul><li><p>The directory itself can become a bottleneck or a single point of failure</p></li><li><p>Cache misses on the directory introduce extra network hops</p></li></ul><p><strong>Best for:</strong> Multi-tenant systems where tenants vary wildly in size.</p><p>Here&#8217;s a quick comparison:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!0rPc!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1541a1b4-2e87-4d25-a79d-6cc355d681fe_1200x1500.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!0rPc!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1541a1b4-2e87-4d25-a79d-6cc355d681fe_1200x1500.png 424w, https://substackcdn.com/image/fetch/$s_!0rPc!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1541a1b4-2e87-4d25-a79d-6cc355d681fe_1200x1500.png 848w, https://substackcdn.com/image/fetch/$s_!0rPc!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1541a1b4-2e87-4d25-a79d-6cc355d681fe_1200x1500.png 1272w, https://substackcdn.com/image/fetch/$s_!0rPc!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1541a1b4-2e87-4d25-a79d-6cc355d681fe_1200x1500.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!0rPc!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1541a1b4-2e87-4d25-a79d-6cc355d681fe_1200x1500.png" width="1200" height="1500" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1541a1b4-2e87-4d25-a79d-6cc355d681fe_1200x1500.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1500,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:991035,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://vincenyanga.me/i/192577480?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1541a1b4-2e87-4d25-a79d-6cc355d681fe_1200x1500.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!0rPc!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1541a1b4-2e87-4d25-a79d-6cc355d681fe_1200x1500.png 424w, https://substackcdn.com/image/fetch/$s_!0rPc!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1541a1b4-2e87-4d25-a79d-6cc355d681fe_1200x1500.png 848w, https://substackcdn.com/image/fetch/$s_!0rPc!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1541a1b4-2e87-4d25-a79d-6cc355d681fe_1200x1500.png 1272w, https://substackcdn.com/image/fetch/$s_!0rPc!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1541a1b4-2e87-4d25-a79d-6cc355d681fe_1200x1500.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>Choosing the Right Shard Key</h2><p>If sharding is the commitment, the shard key is the marriage vow. Get it wrong, and you&#8217;ll feel the pain for years. Here&#8217;s what makes a good shard key:</p><ul><li><p><strong>High cardinality: </strong>The key must have many unique values. A <em>user_id</em> with millions of values is far better than a <em>continent</em> with only 7. Low cardinality literally caps how many effective shards you can have.</p></li><li><p><strong>Even distribution:</strong> The ideal key distributes data roughly equally across all shards.</p></li><li><p><strong>Alignment with query patterns:</strong> Choose a key that appears in your most common WHERE clauses. If most queries filter by <em>customer_id</em>, that should be your shard key. When queries lack a shard key, they become scatter-gather queries that are broadcast to <em>every</em> shard.</p></li><li><p><strong>Immutability:</strong> The shard key should rarely (or never) change. Updating it means physically moving a row between shards.</p></li></ul><h3>Common Mistakes</h3><p>1. <strong>Monotonically increasing keys</strong> (timestamps, auto-increment IDs) with range-based sharding: all new inserts hit the last shard forever, creating a permanent write hotspot.</p><p>2. <strong>Low-cardinality fields</strong> like <em>status</em> or <em>country</em>: you can never have more effective shards than you have distinct values.</p><p>3. <strong>Misalignment with queries:</strong> Sharding by <em>region</em> when queries primarily filter by <em>customer_id</em> forces every customer query to scan all shards.</p><h2>The Cross-Shard Query Problem</h2><p>This is where the real pain lives. Sharding breaks the relational model. If a user&#8217;s account lives on Shard 1 and their transaction record lives on Shard 2, you can&#8217;t use a single database transaction or a simple JOIN.</p><p>Think of it like our library branches: if a researcher needs books from three different branches, they can&#8217;t just walk to one shelf. They need to visit (or call) each branch, collect the results, and piece together what they need.</p><h3>Solutions</h3><p><strong>1. Data Colocation (Prevention is Better Than Cure)</strong></p><p>Design your schema so related data lands on the same shard. For example, if you shard by <em>user_id</em>, make sure a user&#8217;s orders, preferences, and activity logs all use <em>user_id</em> as the shard key. This is the most important technique. Avoid cross-shard queries by design.</p><p>Pinterest does exactly this: when inserting a Pin, they prefer to place it on the same shard as its parent board.</p><p><strong>2. Reference Tables (Broadcast Tables)</strong></p><p>Replicate small, slowly-changing tables (countries, categories, config) across all shards. Joins against reference data become local operations.</p><p><strong>3. Eventual Consistency / CQRS</strong></p><p>Accept that some queries will be eventually consistent. Materialise cross-shard views asynchronously, separating read models from write models.</p><p><strong>4. Modified Two-Phase Commit</strong></p><p>Dropbox&#8217;s Edgestore uses a modified 2PC with an external durable transaction record for cross-shard transactions. The leader writes a durable transaction record before the cross-shard operation, and concurrent requests only need to check this record to determine the transaction state. This approach handles 10 million requests per second across thousands of MySQL nodes, with only 5-10% of transactions being cross-shard.</p><h2>Real-World Examples</h2><h3>Instagram: Embedded Shard IDs</h3><p>Instagram chose sharded PostgreSQL over NoSQL solutions. Their 64-bit ID scheme encodes the shard directly into the primary key:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">| 41 bits: timestamp | 13 bits: shard ID | 10 bits: sequence |</code></pre></div><p>By reading the ID, the application knows exactly which shard to query. No lookup table needed. This supports 8,192 logical shards and 1,024 IDs per millisecond per shard.</p><h3>Discord: From Cassandra to ScyllaDB</h3><p>Discord stored trillions of messages across 177 Cassandra nodes, but hot partitions and JVM garbage collector pauses caused latency spikes. They migrated to ScyllaDB (a Cassandra-compatible database written in C++), reducing their cluster to 72 nodes while dropping p99 read latency from 40-125ms to just 15ms.</p><h3>Slack: Vitess Migration</h3><p>Slack spent three years migrating 99% of their MySQL traffic to Vitess, an open-source sharding middleware. At peak, they handle 2.3 million queries per second (2M reads, 300K writes). The migration eliminated database hotspots and enabled new features like Slack Connect and international data residency.</p><h2>When Should You Actually Shard?</h2><p>Sharding should be a last resort. Before you commit, exhaust these options first:</p><p>1. <strong>Vertical scaling</strong> (more RAM, faster CPU, SSD)</p><p>2. <strong>Read replicas</strong> for read-heavy workloads</p><p>3. <strong>Query optimisation</strong> and better indexing</p><p>4. <strong>Caching layers</strong> (Redis, Memcached)</p><p>5. <strong>Connection pooling</strong></p><p>6. <strong>Table partitioning</strong> (single-node)</p><p>Shard when:</p><ul><li><p>Your data physically doesn&#8217;t fit on one machine</p></li><li><p> Write throughput exceeds what one server can handle</p></li><li><p>You need geographic data residency for compliance (GDPR)</p></li><li><p>You&#8217;ve tried everything above, and it&#8217;s still not enough</p></li></ul><h2>Conclusion</h2><p>Database sharding is a powerful scaling tool, but it&#8217;s not free. It introduces operational complexity: schema changes must be coordinated across all shards, backups become more involved, monitoring must cover every shard, and failure modes multiply.</p><p>The companies that do it well, Instagram, Discord, Slack, Pinterest, all share a common trait: they designed their shard key and data model &#120355;&#120358;&#120359;&#120368;&#120371;&#120358; sharding, not after. They colocated related data, planned for resharding from day one, and built routing layers to keep application code clean.</p><p>If you&#8217;re considering sharding, start with the shard key. Get that right, and most of the other problems become manageable. Get it wrong, and you&#8217;ll have a distributed system that&#8217;s slower and harder to operate than the single database you started with.</p><p>That&#8217;s it for this post. If you want to explore further, I recommend reading Instagram&#8217;s <a href="https://instagram-engineering.com/sharding-ids-at-instagram-1cf5a71e5a5c">engineering blog</a> on their ID generation scheme and Slack&#8217;s <a href="https://slack.engineering/scaling-datastores-at-slack-with-vitess/">write-up</a> on their Vitess migration. Both are excellent examples of sharding done well.</p>]]></content:encoded></item><item><title><![CDATA[Caching Strategies Explained]]></title><description><![CDATA[Choosing the Right One for Your System]]></description><link>https://vincenyanga.me/p/caching-strategies-explained</link><guid isPermaLink="false">https://vincenyanga.me/p/caching-strategies-explained</guid><dc:creator><![CDATA[Vincent Nyanga]]></dc:creator><pubDate>Fri, 01 May 2026 08:01:28 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!277r!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fddb398ba-9934-4156-91cc-d23366d9b858_527x527.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Think of caching like a kitchen prep station. A good chef doesn&#8217;t fetch every ingredient from the pantry for every order. They prep the most-used ingredients and keep them within arm&#8217;s reach. But prep too much, and the food goes stale. Prep the wrong things, and you&#8217;re still running back and forth to the pantry anyway.</p><p>Caching in software works the same way. The right strategy keeps your system fast and your database healthy. The wrong one gives you stale data, wasted memory, or a database that gets crushed the moment a cache key expires.</p><p>In this post, we&#8217;ll walk through the six major caching strategies, when each one shines, and where each one falls apart. We&#8217;ll also cover the common pitfalls that catch teams off guard and how .NET&#8217;s caching stack has evolved to address them.</p><p>Let&#8217;s get started!</p><h2>Cache-Aside (Lazy Loading)</h2><p>This is the strategy most developers learn first, and for good reason. It&#8217;s simple, and it works.</p><p><strong>How it works:</strong></p><p>1. Application receives a read request</p><p>2. Checks the cache for the data</p><p>3. On a hit, returns data from cache</p><p>4. On a miss, queries the database, writes the result to cache, then returns it</p><p>For writes, the application writes directly to the database and invalidates the cache entry. The next read will repopulate it.</p><p><strong>Pros:</strong></p><ul><li><p>Simple to implement and reason about</p></li><li><p>Resilient to cache failures (falls back to the database)</p></li><li><p>Only requested data is cached (no wasted memory)</p></li></ul><p><strong>Cons:</strong></p><ul><li><p>First request for any key always hits the database (cold start)</p></li><li><p> Risk of stale data if invalidation is missed</p></li><li><p>Every service accessing the data must implement the pattern correctly</p></li></ul><p><strong>Best for:</strong> General-purpose read-heavy workloads. E-commerce product catalogues, user profiles, and content management systems.</p><p>Cache-aside is the safe default. If you&#8217;re unsure which strategy to use, start here.</p><h2>Read-Through</h2><p>Read-through looks similar to cache-aside, but there&#8217;s an important difference: the cache itself is responsible for fetching data on a miss, not the application.</p><p><strong>How it works:</strong></p><p>1. Application requests data from the cache</p><p>2. On a hit, the cache returns the data</p><p>3. On a miss, the cache fetches from the database, stores the result, and returns it</p><p>The application never talks to the database directly for reads. It only talks to the cache.</p><p><strong>Pros:</strong></p><ul><li><p>Cleaner application code (no cache-miss logic scattered everywhere)</p></li><li><p>Enforces separation of concerns</p></li></ul><p><strong>Cons:</strong></p><ul><li><p>The cache provider must know how to query your database (tighter coupling)</p></li><li><p>More complex setup and configuration</p></li></ul><p><strong>Best for:</strong> Read-heavy workloads with predictable access patterns. News feeds, product listings, reference data.</p><p>Read-through is often paired with write-through or write-behind for a complete caching solution.</p><h2>Write-Through</h2><p>Write-through is the strategy you reach for when consistency matters more than write speed.</p><p><strong>How it works:</strong></p><p>1. Application writes data to the cache</p><p>2. The cache synchronously writes the same data to the database</p><p>3. Both writes must succeed before the caller gets an acknowledgement</p><p>Because every write goes through the cache first, reads are always up to date. There&#8217;s no stale data window.</p><p><strong>Pros:</strong></p><ul><li><p>Strong consistency between cache and database</p></li><li><p>Reads are always fast (cache is always warm for recently written data)</p></li><li><p>Simple mental model for data freshness</p></li></ul><p><strong>Cons:</strong></p><ul><li><p>Higher write latency (you&#8217;re waiting for two writes on every operation)</p></li><li><p>Write-heavy workloads take a significant performance hit</p></li><li><p>Data that&#8217;s written but rarely read still occupies cache memory</p></li></ul><p><strong>Best for:</strong> Financial systems, inventory management, and user sessions. Anywhere the cost of serving stale data exceeds the cost of slower writes.</p><h2>Write-Behind (Write-Back)</h2><p>Write-behind flips the consistency tradeoff. It prioritises write speed and accepts eventual consistency.</p><p><strong>How it works:</strong></p><p>1. Application writes data to the cache</p><p>2. Cache immediately acknowledges the write</p><p>3. In the background, the cache batches and flushes writes to the database asynchronously</p><p>The application never waits for the database write to complete. This gives you the lowest write latency of any strategy.</p><p><strong>Pros:</strong></p><ul><li><p>Extremely low write latency</p></li><li><p>Batching reduces database load (10 individual writes become 1 batch insert)</p></li><li><p>Excellent for write-heavy workloads</p></li></ul><p><strong>Cons:</strong></p><ul><li><p>If the cache node crashes before flushing, that data is lost</p></li><li><p>Eventual consistency between cache and database</p></li><li><p>Debugging is harder (database state lags behind cache state)</p></li></ul><p><strong>Best for:</strong> Analytics event ingestion, social media activity feeds (likes, views, impressions), IoT sensor data, logging systems.</p><p>I need to emphasise: only use write-behind when you can tolerate some data loss, or when you have cache replication in place as a safety net.</p><h2>Write-Around</h2><p>Write-around is the quiet one. It doesn&#8217;t get talked about much, but it solves a specific and common problem: cache pollution.</p><p><strong>How it works:</strong></p><p>1. Application writes data directly to the database, bypassing the cache entirely</p><p>2. The cache is not updated on writes</p><p>3. Reads follow cache-aside or read-through; data enters the cache only when it&#8217;s actually requested</p><p><strong>Pros:</strong></p><ul><li><p>Prevents the cache from filling up with data that&#8217;s written once and never read</p></li><li><p>Cache memory is reserved for frequently accessed data</p></li><li><p>Simple write path</p></li></ul><p><strong>Cons:</strong></p><ul><li><p>First read after a write always hits the database</p></li><li><p>Not suitable if writes are immediately followed by reads</p></li></ul><p><strong>Best for:</strong> Log ingestion, audit trails, batch data imports, and real-time chat message storage. Any workload where you write far more data than you read.</p><h2>Refresh-Ahead</h2><p>Refresh-ahead is the proactive strategy. Instead of waiting for a cache entry to expire and then suffering a miss, it refreshes entries before they expire.</p><p><strong>How it works:</strong></p><p>1. Application reads from cache as normal</p><p>2. When an entry is within a configurable window before expiration (say, 80% through its TTL), the cache returns the current value immediately and triggers an asynchronous background refresh</p><p>3. The background job fetches fresh data and updates the cache before the TTL expires</p><p><strong>Pros:</strong></p><ul><li><p>Eliminates latency spikes for popular keys</p></li><li><p>Predictable, consistent response times</p></li><li><p>Users always hit a warm cache for hot data</p></li></ul><p><strong>Cons:</strong></p><ul><li><p>Wasted refreshes for entries that nobody accesses again</p></li><li><p>Increased backend load from proactive fetches</p></li><li><p>Requires predictable access patterns to be cost-effective</p></li></ul><p><strong>Best for:</strong> Dashboards, leaderboards, stock tickers, and news homepage data. Anywhere you have high-traffic keys that must always be fast.</p><h2>Combining Strategies</h2><p>In practice, most production systems don&#8217;t use a single strategy. They combine them:</p><ul><li><p><strong>Cache-Aside + Write-Around</strong>: the most common pairing. Reads are cached on demand. Writes bypass the cache. Simple and effective for most CRUD applications.</p></li><li><p><strong>Read-Through + Write-Through</strong>: full cache mediation. The application never touches the database directly. Strong consistency with clean code.</p></li><li><p><strong>Read-Through + Write-Behind</strong>: high-throughput systems that need both fast reads and fast writes, and can tolerate eventual consistency.</p></li><li><p><strong>Cache-Aside + Refresh-Ahead</strong>: for the critical hot paths. Most data uses regular cache-aside. The top 1% of keys get a proactive refresh.</p></li></ul><p>The right combination depends on your consistency requirements, read/write ratio, and tolerance for complexity.</p><h2>The Pitfalls Nobody Warns You About</h2><h3>Cache Stampede (Thundering Herd)</h3><p>A popular cache key expires. Hundreds of concurrent requests see the miss, and all hit the database simultaneously.</p><p>This is exactly what caused one of Facebook&#8217;s biggest outages in 2010. A configuration change invalidated a frequently-accessed cache entry. The resulting stampede overwhelmed their database, and error-handling logic made it worse by deleting more cache keys, creating a self-reinforcing cascade that lasted 2.5 hours.</p><p><strong>Solutions:</strong> distributed locking (only one request fetches, others wait), probabilistic early expiration (random chance of refreshing before TTL), or use .NET&#8217;s HybridCache, which handles this automatically with built-in stampede protection.</p><h3>Cache Avalanche</h3><p>Many keys expire at the same time. This typically happens when you set the same TTL on everything at startup.</p><p><strong>Solution:</strong> Add random jitter to your TTLs. Instead of 10 minutes for every key, use 10 minutes + a random 0-60 seconds.</p><h3>Cache Penetration</h3><p>Requests for keys that don&#8217;t exist in the cache or the database. Every request passes through to the database. Often caused by bots or bugs.</p><p><strong>Solution:</strong> Cache null results with a short TTL. For heavy traffic, use a Bloom filter to quickly reject keys that are known not to exist.</p><h2>.NET&#8217;s Caching Stack in 2026</h2><p>The .NET caching story has matured significantly:</p><ul><li><p><strong>IMemoryCache</strong>: in-process, single-server. Fast, but lost on restart and not shared across instances.</p></li><li><p><strong>IDistributedCache</strong>: abstraction over Redis, SQL Server, and CosmosDB. Shared across instances but requires serialisation.</p></li><li><p><strong>HybridCache (.NET 9+)</strong>: the new recommended default. Combines L1 (in-process) + L2 (distributed) with stampede protection and tag-based invalidation out of the box.</p></li></ul><p>HybridCache deserves special attention. It eliminates the hand-rolled cache-aside boilerplate that&#8217;s in every .NET codebase:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;csharp&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-csharp">// Before: manual cache-aside

var product = await cache.GetAsync&lt;Product&gt;($&#8221;product-{id}&#8221;);

if (product is null)
{
  product = await db.Products.FindAsync(id);

  await cache.SetAsync($&#8221;product-{id}&#8221;, product,

  new DistributedCacheEntryOptions

  {

    AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)

  });

}

// After: HybridCache

var product = await cache.GetOrCreateAsync(
  $&#8221;product-{id}&#8221;,
  async token =&gt; await db.Products.FindAsync(id, token),
  tags: [&#8221;products&#8221;, $&#8221;category-{categoryId}&#8221;]
);

// Bulk invalidation by tag
await cache.RemoveByTagAsync($&#8221;category-{categoryId}&#8221;);</code></pre></div><p>If 100 requests arrive for the same missing key simultaneously, HybridCache runs the factory method once and returns the same result to all 100 requests. No stampede. No duplicate database calls.</p><h2>Conclusion</h2><p>Caching is not a single tool. It&#8217;s a toolkit. Cache-aside is the safe starting point, but knowing when to reach for write-through, write-behind, or refresh-ahead is what separates systems that scale from systems that buckle under load.</p><p>Like everything in software engineering, one needs to weigh the pros and cons. Strong consistency costs write latency. Low write latency risks data loss. Proactive refresh costs backend resources. There&#8217;s no free lunch.</p><p>My advice: start with cache-aside. Add complexity only when you have evidence that it&#8217;s needed. Always set TTLs as a safety net. And if you&#8217;re on .NET 9+, make HybridCache your default; it handles the hardest problems (stampede protection, L1+L2, tag invalidation) so you don&#8217;t have to.</p>]]></content:encoded></item><item><title><![CDATA[Full-Text Search with PostgreSQL]]></title><description><![CDATA[The Search Engine Already in Your Database]]></description><link>https://vincenyanga.me/p/full-text-search-with-postgresql</link><guid isPermaLink="false">https://vincenyanga.me/p/full-text-search-with-postgresql</guid><dc:creator><![CDATA[Vincent Nyanga]]></dc:creator><pubDate>Fri, 17 Apr 2026 08:00:53 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!277r!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fddb398ba-9934-4156-91cc-d23366d9b858_527x527.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When someone says &#8220;we need search&#8221;, the conversation almost always jumps to Elasticsearch or Algolia. It&#8217;s a reflex at this point. But here&#8217;s the thing: PostgreSQL has had a full-text search engine baked into its core since version 8.3, released in 2008. That&#8217;s 18 years of battle-tested search sitting right inside the database you&#8217;re probably already running.</p><p>In this post, we&#8217;ll explore how PostgreSQL full-text search works, when it&#8217;s the right choice, and how to set it up with practical code examples. By the end, you&#8217;ll have everything you need to build a solid search feature without spinning up a single extra service.</p><h2>How PostgreSQL Full-Text Search Works</h2><p>At its core, Postgres full-text search revolves around two data types: <strong>tsvector</strong> and <strong>tsquery</strong>.</p><p>A <strong>tsvector</strong> is a sorted list of distinct lexemes (think of them as normalised words). When you convert text into a tsvector, Postgres applies language-specific processing: it strips stop words like &#8220;the&#8221; and &#8220;is&#8221;, and it stems words to their root form. &#8220;Running&#8221;, &#8220;runs&#8221;, and &#8220;ran&#8221; all become &#8220;run&#8221;.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT to_tsvector(&#8217;english&#8217;, &#8216;The quick brown foxes were jumping&#8217;);

-- Result: &#8216;brown&#8217;:3 &#8216;fox&#8217;:4 &#8216;jump&#8217;:5 &#8216;quick&#8217;:2</code></pre></div><p>Notice how &#8220;foxes&#8221; became &#8220;fox&#8221; and &#8220;jumping&#8221; became &#8220;jump&#8221;. The numbers represent positions in the original text.</p><p>A <strong>tsquery</strong> represents your search query. It supports Boolean operators like `&amp;` (AND), `|` (OR), `!` (NOT), and `&lt;-&gt;` (FOLLOWED BY) for phrase matching.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT to_tsquery(&#8217;english&#8217;, &#8216;quick &amp; fox&#8217;);

-- Result: &#8216;quick&#8217; &amp; &#8216;fox&#8217;</code></pre></div><p>To check whether a document matches a query, you use the `@@` operator:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT to_tsvector(&#8217;english&#8217;, &#8216;The quick brown fox&#8217;)

@@ to_tsquery(&#8217;english&#8217;, &#8216;quick &amp; fox&#8217;);

-- Result: true</code></pre></div><p>Simple enough. But this alone would be slow on a real table. That&#8217;s where indexes come in.</p><h2>Setting Up Search: The Three-Step Pattern</h2><p>In my experience, you need exactly three things to get production-ready full-text search in Postgres: a tsvector column, a GIN index, and a ranking query. Let&#8217;s walk through each.</p><h3>Step 1: Add a Generated tsvector Column</h3><p>Since PostgreSQL 12, you can use a generated column that automatically computes the tsvector whenever the row changes. This is the cleanest approach because there are no triggers to maintain.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">CREATE TABLE articles (

id SERIAL PRIMARY KEY,

title VARCHAR(255) NOT NULL,

body TEXT NOT NULL,

author VARCHAR(100),

search_vector tsvector GENERATED ALWAYS AS (

setweight(to_tsvector(&#8217;english&#8217;, coalesce(title, &#8216;&#8217;)), &#8216;A&#8217;) ||

setweight(to_tsvector(&#8217;english&#8217;, coalesce(body, &#8216;&#8217;)), &#8216;B&#8217;) ||

setweight(to_tsvector(&#8217;english&#8217;, coalesce(author, &#8216;&#8217;)), &#8216;C&#8217;)

) STORED
);
</code></pre></div><p>The `setweight()` function assigns importance levels. Weight &#8216;A&#8217; is the highest, &#8216;D&#8217; the lowest. Here, title matches will rank higher than body matches, which rank higher than author matches. This is exactly what users expect from search results.</p><h3>Step 2: Create a GIN Index</h3><p>A <strong>GIN (Generalised Inverted Index)</strong> works like the index at the back of a textbook. It maps each word to a list of rows containing that word. Without this index, Postgres would need to scan every row and compute tsvectors on the fly.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">CREATE INDEX idx_articles_search ON articles USING GIN (search_vector);</code></pre></div><p>GIN indexes are about 3x faster for lookups than the alternative GiST index. The trade-off is that they&#8217;re slower to build and larger on disk, but for read-heavy search workloads (which is most search workloads), GIN is the right choice.</p><h3>Step 3: Query with Ranking</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT id, 
  title,
  ts_rank(search_vector, query) AS rank

FROM articles, websearch_to_tsquery(&#8217;english&#8217;, &#8216;database performance&#8217;) AS query

WHERE search_vector @@ query

ORDER BY rank DESC

LIMIT 20;</code></pre></div><p>`websearch_to_tsquery` is a gem that was introduced in PostgreSQL 11. It accepts Google-like search syntax: quoted phrases for exact match, `-` for exclusion, and implicit AND between words. It handles messy user input gracefully, which makes it perfect for user-facing search boxes.</p><p>Want to show highlighted snippets? Use `ts_headline`:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT id,
     title,
     ts_headline(&#8217;english&#8217;, 
     body,
     websearch_to_tsquery(&#8217;english&#8217;, &#8216;database performance&#8217;),
    &#8216;StartSel=&lt;mark&gt;, StopSel=&lt;/mark&gt;, MaxFragments=3&#8217;
) AS snippet
FROM articles
WHERE search_vector @@ websearch_to_tsquery(&#8217;english&#8217;, &#8216;database performance&#8217;);

</code></pre></div><h2>Handling Typos with pg_trgm</h2><p>One legitimate gap in Postgres FTS is its lack of typo tolerance. If a user searches for &#8220;postgressql&#8221; (note the typo), the full-text search won&#8217;t find &#8220;postgresql&#8221;. This is where the <strong>pg_trgm</strong> extension comes in.</p><p>Trigram matching breaks words into three-character sequences and compares them. It doesn&#8217;t care about stemming or language; it just measures how similar two strings look.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">CREATE EXTENSION IF NOT EXISTS pg_trgm;

-- Create a trigram index on the title

CREATE INDEX idx_articles_title_trgm ON articles USING GIN (title gin_trgm_ops);

-- Fuzzy search

SELECT title, similarity(title, &#8216;postgressql&#8217;) AS sim

FROM articles

WHERE title % &#8216;postgressql&#8217;

ORDER BY sim DESC;</code></pre></div><p>The real power comes from combining both approaches:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT id, 
  title,
  ts_rank(search_vector,
  websearch_to_tsquery(&#8217;english&#8217;, &#8216;postgres&#8217;)) * 2.0 + similarity(title, &#8216;postgres&#8217;) AS combined_score

FROM articles

WHERE search_vector @@ websearch_to_tsquery(&#8217;english&#8217;, &#8216;postgres&#8217;)

OR title % &#8216;postgres&#8217;

ORDER BY combined_score DESC

LIMIT 20;</code></pre></div><p>This gives you relevance-ranked results from full-text search, with a fallback to fuzzy matching for typos. It covers a surprising amount of ground.</p><h2>Multi-Table Search with Materialised Views</h2><p>Things get more interesting when you need to search across multiple tables. Imagine an e-commerce app where you want to search products by name, description, category, and tags. Joining four tables on every search query would be painfully slow.</p><p>The solution is a materialised view that precomputes the search vector:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">CREATE MATERIALIZED VIEW product_search AS

SELECT p.id,
   p.name, 
   p.description, 
   c.name AS category_name,
   string_agg(t.name, &#8216; &#8216;) AS tag_names,

setweight(to_tsvector(&#8217;english&#8217;, p.name), &#8216;A&#8217;) ||

setweight(to_tsvector(&#8217;english&#8217;, coalesce(p.description, &#8216;&#8217;)), &#8216;B&#8217;) ||

setweight(to_tsvector(&#8217;english&#8217;, coalesce(c.name, &#8216;&#8217;)), &#8216;C&#8217;) ||

setweight(to_tsvector(&#8217;english&#8217;, coalesce(string_agg(t.name, &#8216; &#8216;), &#8216;&#8217;)), &#8216;D&#8217;)

AS search_vector

FROM products p

JOIN categories c ON p.category_id = c.id

LEFT JOIN product_tags pt ON p.id = pt.product_id

LEFT JOIN tags t ON pt.tag_id = t.id

GROUP BY p.id, p.name, p.description, c.name;

CREATE UNIQUE INDEX ON product_search (id);

CREATE INDEX ON product_search USING GIN (search_vector);</code></pre></div><p>Now searches hit a single, pre-indexed table. Refresh it periodically with `REFRESH MATERIALIZED VIEW CONCURRENTLY` (the `CONCURRENTLY` keyword keeps the view available during refresh, so there&#8217;s no downtime).</p><p>The trade-off is that results can be slightly stale, usually by minutes. For most applications, however, that&#8217;s acceptable.</p><h2>When Postgres FTS Is Enough (and When It Isn&#8217;t)</h2><p>Here&#8217;s my rule of thumb: if search is a <strong>feature</strong> of your app, use Postgres. If search <strong>is</strong> your app, consider a dedicated engine.</p><p><strong>Postgres FTS shines when:</strong></p><ul><li><p>Your dataset is under a few million rows</p></li><li><p>You want zero additional infrastructure</p></li><li><p>ACID compliance matters (no stale search results from sync lag)</p></li><li><p>Your team is small and can&#8217;t afford to maintain a separate search cluster</p></li></ul><p><strong>You might outgrow it when:</strong></p><ul><li><p>Search relevancy is the core product experience (e-commerce, content discovery)</p></li><li><p>You need built-in autocomplete suggestions, faceted navigation, or synonym handling</p></li><li><p>Your dataset exceeds tens of millions of rows with sub-50ms latency requirements</p></li></ul><p>It&#8217;s also worth mentioning the extension ecosystem. Projects like <strong>pg_textsearch</strong> (from Timescale) bring BM25 ranking (the same algorithm Elasticsearch uses) directly into Postgres, with 4x faster top-k queries and 41% smaller indexes. <strong>ParadeDB&#8217;s pg_search</strong> is another option, built on a Rust-based search engine, delivering 20x faster ranking than native tsvector.</p><p>The line between &#8220;need a dedicated engine&#8221; and &#8220;Postgres is fine&#8221; keeps moving in Postgres&#8217;s favour.</p><h2>Conclusion</h2><p>PostgreSQL full-text search is one of the most underutilised features in the database world. A generated tsvector column, a GIN index, and websearch_to_tsquery give you stemming, ranking, phrase search, and multi-language support with no extra infrastructure. Add pg_trgm for typo tolerance and materialised views for multi-table search, and you&#8217;ve covered what most applications need.</p><p>I have created a simple project that showcases how all this works. You can check it out on <a href="https://github.com/vince-nyanga/postgres-fts">GitHub</a>.&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</p>]]></content:encoded></item><item><title><![CDATA[Vector Search vs Keyword Search: Choose the Right Tool, Not the Trendy One]]></title><description><![CDATA[Imagine you&#8217;re looking for a book in two different libraries.]]></description><link>https://vincenyanga.me/p/vector-search-vs-keyword-search-choose</link><guid isPermaLink="false">https://vincenyanga.me/p/vector-search-vs-keyword-search-choose</guid><dc:creator><![CDATA[Vincent Nyanga]]></dc:creator><pubDate>Fri, 03 Apr 2026 08:00:32 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!277r!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fddb398ba-9934-4156-91cc-d23366d9b858_527x527.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Imagine you&#8217;re looking for a book in two different libraries. The first library has a perfect card catalogue; every word in every book is indexed. You walk in, say &#8220;ERROR 1045&#8221;, and the librarian hands you the exact page. Fast, precise, surgical.</p><p>The second library has a librarian with deep reading comprehension. You walk in and say, &#8220;I can&#8217;t connect to my database,&#8221; and she understands what you mean, walks you to the right shelf, and pulls three books that might help, even if none of them uses the phrase &#8220;can&#8217;t connect.&#8221;</p><p>Both librarians are valuable. The mistake is assuming the second one makes the first one obsolete.</p><p>That&#8217;s the vector search vs keyword search debate in a nutshell. And in this post, I&#8217;ll break down exactly when each approach wins, where hybrid search bridges the gap, and why choosing vector search purely because it sounds more &#8220;AI&#8221; is how you build an expensive solution to the wrong problem.</p><p>Let&#8217;s get started.</p><h2>What is Keyword Search?</h2><p><strong>Keyword search</strong> (also called lexical search) finds documents by matching the exact tokens in your query against an inverted index of your corpus. The dominant algorithm is <strong>BM25</strong> (Best Match 25), a ranking function that weighs matches by term frequency and how rare the term is across the document collection.</p><p>BM25 is 30 years old. It powers Elasticsearch, OpenSearch, and Solr. It needs no model, no GPU, no embedding API. It&#8217;s deterministic, the same query always returns the same results, and it&#8217;s fast. Sub-millisecond retrieval at millions of documents.</p><p>It is, to put it plainly, still extremely good.</p><h2>What is Vector Search?</h2><p><strong>Vector search</strong> (semantic search) converts your documents and queries into high-dimensional numerical vectors using an embedding model, something like OpenAI&#8217;s `text-embedding-3-small`, Cohere Embed, or open-source models like E5 or BGE. Once embedded, documents and queries live in the same vector space, and similarity is measured by cosine similarity or dot product.</p><p>The key insight: semantically similar content ends up close together in vector space, even if the words are completely different. &#8220;I can&#8217;t log in&#8221; and &#8220;authentication failure&#8221; end up near each other. BM25 would miss that connection entirely.</p><p>Vector search enables retrieval based on <em>meaning</em>, not just tokens. That&#8217;s genuinely powerful. But it comes with a cost that teams consistently underestimate.</p><h2>When Keyword Search Wins</h2><p>This is the part that gets skipped in most &#8220;AI search&#8221; blog posts.</p><p><strong>Exact Matches That Must Be Exact</strong></p><p>If a user searches for `ERROR 1045`, they want documents containing exactly that. A vector search might surface related database authentication errors conceptually relevant, but not the one the user typed. The same applies to:</p><p>- Product SKUs: &#8220;iPhone 15 Pro Max 256GB&#8221; must return that exact model, not the nearest semantic neighbour</p><p>- Order IDs, serial numbers, account numbers</p><p>- Medical codes (ICD-10), legal citations, regulatory references</p><p>- API function names, config flags, library import paths</p><p>In these cases, a 30x performance advantage for BM25 and exact precision makes it the obvious choice.</p><p><strong>Cost and Operational Reality</strong></p><p>Vector search has hidden costs that only reveal themselves in production:</p><ul><li><p><strong>Embedding latency: </strong>Small embedding models run at ~16ms. Large models (7B+ parameters) sit at 187-221ms &#8212; over 10x slower. At user-facing latency budgets, this matters significantly.</p></li><li><p><strong>Infrastructure overhead:</strong> You need an embedding model (hosted or self-managed), a vector database, and a sync pipeline to keep it up to date. BM25 runs on Elasticsearch you&#8217;re already operating.</p></li><li><p><strong>Embedding model lock-in:</strong> If you switch embedding models, you must re-embed your entire document corpus. New query vectors won&#8217;t align with old document vectors. That&#8217;s a silent, expensive migration with no warning signs until results degrade.</p></li><li><p><strong>Index degradation at scale:</strong> HNSW (the algorithm powering most vector DBs). Recall can drop by 10%+ as the database grows from 50k to 200k vectors. Your infrastructure dashboards look fine. Your search quality is quietly getting worse.</p></li></ul><h2>When Vector Search Wins</h2><p>Vector search earns its complexity in specific scenarios.</p><p><strong>The Words Don&#8217;t Match</strong></p><p>The most common scenario: a user describes what they need without knowing the exact terminology used in your documents. &#8220;My app is getting slow under load&#8221; should surface articles about performance optimisation, connection pooling, and caching strategies, even if none of them uses the phrase &#8220;getting slow.&#8221;</p><p>This is where keyword search fails completely, and vector search excels.</p><p><strong>Multilingual Search</strong></p><p>Embedding models are trained on multilingual corpora. A query in English can retrieve semantically similar documents written in Spanish, French, or German. BM25 requires explicit multilingual tokenisation pipelines to even attempt this.</p><p><strong>Recommendation and Similarity</strong></p><p>&#8220;More like this article&#8221; queries, duplicate detection, and recommendation engines are a natural fit for vector similarity. Find the documents closest to a given embedding; there&#8217;s no keyword equivalent.</p><h2>The Real Answer: Hybrid Search</h2><p>Here&#8217;s the thing neither camp wants to admit: the best production search systems use both.</p><p><strong>**Hybrid search**</strong> combines BM25 keyword results and vector search results, then merges them using <strong>**Reciprocal Rank Fusion (RRF)**</strong>. Instead of trying to normalise incompatible scores, RRF uses <em>rank position</em>. The formula:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">score = 1 / (rank + k)</code></pre></div><p>Where `rank` is the document&#8217;s position in either result list, and `k` (typically 60) prevents top-ranked documents from dominating too aggressively.</p><p>A document appearing at position 1 in the BM25 list and position 2 in the vector list scores very high. A document appearing at position 1 in only one list scores lower. The result: exact-match precision from BM25, semantic recall from vector search, merged into a single ranked list that consistently outperforms either approach alone.</p><p>Here&#8217;s what this looks like in practice with LangChain:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from langchain.retrievers import BM25Retriever, EnsembleRetriever

from langchain_community.vectorstores import Chroma

# BM25 retriever over your document corpus

bm25_retriever = BM25Retriever.from_documents(docs)

bm25_retriever.k = 5

# Vector retriever with your embedding model

vectorstore = Chroma.from_documents(docs, embedding_function)

vector_retriever = vectorstore.as_retriever(search_kwargs={&#8221;k&#8221;: 5})

# Hybrid retriever using RRF (weights: 50/50)

ensemble_retriever = EnsembleRetriever(

retrievers=[bm25_retriever, vector_retriever],

weights=[0.5, 0.5]

)

results = ensemble_retriever.invoke(&#8221;authentication timeout error&#8221;)</code></pre></div><p>This retriever does the right thing for both query types: a search for &#8220;ERROR 1045&#8221; gets exact BM25 precision; a search for &#8220;why does my login keep failing&#8221; gets semantic vector recall. You don&#8217;t have to choose.</p><p>You can tune the weights based on your domain. Code search might run 70% BM25 / 30% vector. A customer support chatbot might flip to 30% BM25 / 70% vector. The EnsembleRetriever makes this trivially adjustable.</p><p>Native hybrid search is now supported in: Azure AI Search, Elasticsearch 8+, OpenSearch 2.19, Weaviate, Chroma, pgvector + pg_bm25 (ParadeDB), SingleStore, and MariaDB.</p><h2>The Decision Framework</h2><p>Like everything in software engineering, one needs to weigh the pros and cons. Here&#8217;s a practical guide:</p><ul><li><p> <strong>Error codes, SKUs, order IDs</strong>: Use keyword only (BM25) </p></li><li><p><strong>Code search, API documentation</strong>:  Use keyword-dominant hybrid (70/30) </p></li><li><p><strong>Customer support FAQ</strong>:  Use balanced hybrid (50/50)</p></li><li><p><strong>Conversational / intent-driven search</strong>: Use vector-dominant hybrid (30/70) </p></li><li><p><strong>&#8220;More like this&#8221; recommendations</strong>: Use vector only </p></li><li><p><strong>Multilingual search</strong>: Use vector only </p></li></ul><p>I need to emphasise this: the question is never &#8220;which is better?&#8221; The question is &#8220;what does my query distribution actually look like, and what does my current infrastructure already support?&#8221;</p><h2>Conclusion</h2><p>Vector search is a genuinely powerful tool. Embedding-based retrieval unlocks search experiences that keyword matching simply cannot deliver. I&#8217;m not arguing against it.</p><p>But I&#8217;ve seen teams rip out working Elasticsearch deployments to stand up a vector database, a managed embedding API, a sync pipeline, and a new infrastructure dependency, for a search use case that was doing just fine with BM25.</p><p>The hype around vector search is real. The problems it solves are real. So are the costs, the operational complexity, and the cases where a 30-year-old ranking function still does the job better.</p><p>Don&#8217;t choose based on what sounds more modern. Choose based on what your users are actually searching for. And if you&#8217;re unsure, hybrid search gives you both, with a tunable dial between precision and recall.</p><p>Start there. Optimise from data, not from trends.</p>]]></content:encoded></item><item><title><![CDATA[CQRS: The Pattern That Sounds Simple Until You Ship It to Production]]></title><description><![CDATA[&#8220;For most systems, CQRS adds risky complexity.&#8221; Those were the words of Martin Fowler, and I fully agree with him.]]></description><link>https://vincenyanga.me/p/cqrs-the-pattern-that-sounds-simple</link><guid isPermaLink="false">https://vincenyanga.me/p/cqrs-the-pattern-that-sounds-simple</guid><dc:creator><![CDATA[Vincent Nyanga]]></dc:creator><pubDate>Fri, 20 Mar 2026 08:01:02 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!277r!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fddb398ba-9934-4156-91cc-d23366d9b858_527x527.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>&#8220;For most systems, CQRS adds risky complexity.&#8221; Those were the words of Martin Fowler, and I fully agree with him. Yet teams keep implementing it, often for all the wrong reasons. Here&#8217;s the uncomfortable reality: CQRS is a high-complexity pattern that separates read and write operations into distinct models. When applied unnecessarily, it becomes an expensive architectural mistake you&#8217;ll pay for with every sprint.</p><p>But when applied correctly, to the right problems, at the right organisational maturity level? CQRS enables capabilities that traditional architectures simply can&#8217;t match. The trick is knowing the difference before you commit.</p><h2>Pattern Fundamentals: What You&#8217;re Actually Building</h2><p>CQRS divides systems into Commands (operations that change state) and Queries (read-only operations that return data). This separation enables independent optimisation: write models focus on transactional integrity, while read models optimise for query performance.</p><p><strong>Logical CQRS:</strong> maintains separation only at the code level, using the same database for reads and writes. This delivers architectural clarity without operational complexity, which is ideal for teams that want clear boundaries without eventual consistency headaches.</p><p><strong>Physical CQRS</strong> uses separate databases, enabling independent scaling and storage optimisation. Your write side might use PostgreSQL for transactional integrity, while your read side leverages MongoDB and Elasticsearch. The benefit is powerful. The cost? Eventual consistency, synchronisation complexity, and operational overhead are often severely underestimated by many teams.</p><h2>CQRS + Event Sourcing: Double or Nothing</h2><p>Event Sourcing stores state as a chronological sequence of events rather than a current snapshot. Combined with CQRS, commands generate events stored in event stores (EventStoreDB, Kafka, etc.), which projections consume to build optimised read models.</p><p>This combination provides capabilities that sound transformative: complete audit trails, time-travel debugging by replaying events, and multiple read model projections from the same event stream.</p><p>The reality check arrives in production. You&#8217;re now managing sophisticated projection systems, event schema versioning, and operational complexity requiring expertise most teams don&#8217;t possess. Event Sourcing isn&#8217;t &#8220;CQRS plus some extras&#8221;. It&#8217;s a fundamentally different architectural commitment with substantially higher implementation risk.</p><h2>The Decision Framework: When CQRS Makes Sense (And When It Doesn&#8217;t)</h2><h3>Implement CQRS when:</h3><p>- Your domain exhibits genuine complexity, benefiting from Domain-Driven Design bounded contexts</p><p>- Read and write patterns are dramatically imbalanced, requiring independent scaling</p><p>- Different optimisation approaches for commands versus queries provide measurable value</p><p>- You&#8217;re building read-heavy applications with large analytical reports benefiting from pre-aggregated data</p><h3>Avoid CQRS when:</h3><p>- Your domain is simple, and CRUD interfaces suffice</p><p>- Teams lack distributed systems experience</p><p>- You&#8217;re building an MVP where speed to market trumps architectural sophistication</p><p>- Your application requires real-time consistency</p><p>As one architect who lived through a failed CQRS migration put it: &#8220;We spent six months implementing CQRS for a glorified CRUD app. It didn&#8217;t make us faster. It made us slower, indefinitely.&#8221;</p><h2>Critical Pitfalls and How to Avoid Them</h2><p><strong>Over-Engineering Simple CRUD:</strong> The most common failure mode involves applying CQRS to systems that fit traditional data models perfectly. You&#8217;ve consumed development velocity due to synchronisation issues and operational overhead that outweigh any benefits. If your application is primarily forms-based rather than database-driven, CQRS is the wrong choice.</p><p><strong>Ignoring Eventual Consistency:</strong> Applications showing stale data without loading indicators appear broken to users. One e-commerce team discovered customers abandoning carts because the UI showed outdated inventory. Solution: Version-based synchronisation returns version numbers with command results, then blocks queries until projections reach the requested version.</p><p><strong>Poor Boundary Design:</strong> Unclear ownership between command and query models leads to duplicate logic and coupling. Treat events as first-class APIs requiring versioning discipline and backward compatibility.</p><p><strong>Legacy Integration:</strong> CQRS architectures struggle with unmovable legacy components. If your architecture depends on deep legacy integration, CQRS complexity may outweigh separation benefits.</p><h2>Production Challenges: The Hard Parts Nobody Mentions</h2><h3>Eventual Consistency Creates Real UX Problems</h3><p>Updates to read stores lag behind event generation by milliseconds to seconds during high load. Users are redirected to dashboards after commands, and see nothing. This creates frustrating experiences in which applications appear broken even though they&#8217;re working exactly as designed.</p><p>The solution requires sophisticated implementation. Return version numbers with command responses. Implement wait handles in query handlers that block until projections reach the requested version. Design UIs with loading states, optimistic updates, or clear indicators that data is propagating.</p><p>Some domains can&#8217;t tolerate this. Financial transactions, inventory decrements, and real-time booking systems require immediate consistency. CQRS&#8217;s eventual consistency model creates unacceptable risk in these scenarios.</p><h3>Projection Failures Require Sophisticated Recovery</h3><p>Projections will fail due to network partitions, schema mismatches, resource exhaustion, and other factors. Distributed systems have failure modes that single-database applications never encounter. You need to monitor the tracking event store's disk usage, replication lag, projection latency, and failures. Health checks verifying projection completeness&#8212;replay capabilities. The most straightforward approach is to truncate read models and reapply all events.</p><p>One team running CQRS reported spending 40% more time on monitoring infrastructure compared to their previous monolithic architecture.</p><h3>Event Schema Evolution Is a First-Class Problem</h3><p>Events are immutable. Once written, they live in your event store forever. When business requirements change, you can&#8217;t just alter a database schema. You&#8217;re managing a versioned event catalogue that projections must interpret correctly across all historical versions.</p><p>Strategies that work: Event upcasting converts older versions to the current format during deserialization. Additive changes maintain backward compatibility. Version stamps maintain both formats when breaking changes are unavoidable. Event handlers must support all event versions your store contains.</p><p>This versioning discipline becomes a permanent tax on development velocity.</p><h3>Distributed Tracing Becomes Non-Negotiable</h3><p>Debugging request flows across CQRS boundaries without distributed tracing is archaeological work. Correlation IDs tracking operations from initial commands through read model updates are essential. Teams report debugging time increases by 35% in CQRS architectures compared to modular monoliths. The only way to manage this is world-class observability infrastructure from day one.</p><h2>The Bottom Line</h2><p>CQRS is a powerful pattern that solves specific problems at specific scales with specific team capabilities. It&#8217;s not a default architectural choice. It&#8217;s a high-complexity optimisation that makes sense when organisational maturity, domain complexity, and scaling requirements justify the investment.</p><p>The gap between CQRS success and failure isn&#8217;t understanding the pattern. It&#8217;s honestly assessing whether your context justifies its complexity. If it doesn&#8217;t, then you probably shouldn&#8217;t use it.</p><p>If you&#8217;re considering CQRS, the most critical question isn&#8217;t &#8220;how do we implement this?&#8221; It&#8217;s &#8220;Are we sure we need this?&#8221; Answer honestly, and you&#8217;ll save yourself months of complexity providing zero business value.</p><p><strong>Further Reading</strong></p><p>- <a href="https://learn.microsoft.com/en-us/azure/architecture/patterns/cqrs">Microsoft Learn - CQRS Pattern</a></p><p>- <a href="https://www.martinfowler.com/bliki/CQRS.html">Martin Fowler - CQRS</a></p><p>- <a href="https://learn.microsoft.com/en-us/previous-versions/msp-n-p/jj554200(v=pandp.10">Microsoft CQRS Journey</a></p><p>- <a href="https://www.confluent.io/blog/event-sourcing-cqrs-stream-processing-apache-kafka-whats-connection/">Confluent</a> - Event Sourcing, CQRS, Stream Processing and Apache Kafka</p><p>- <a href="https://event-driven.io/en/cqrs_facts_and_myths_explained/">Event-Driven.io </a>- CQRS Facts and Myths Explained</p><p>- <a href="https://www.techtarget.com/searchapparchitecture/tip/Common-CQRS-pattern-problems-and-how-teams-can-avoid-them">TechTarget</a> - 3 Common CQRS Pattern Problems</p>]]></content:encoded></item><item><title><![CDATA[Why Your Brain Makes You Procrastinate (And What Actually Works to Stop It)]]></title><description><![CDATA[You know the feeling.]]></description><link>https://vincenyanga.me/p/why-your-brain-makes-you-procrastinate</link><guid isPermaLink="false">https://vincenyanga.me/p/why-your-brain-makes-you-procrastinate</guid><dc:creator><![CDATA[Vincent Nyanga]]></dc:creator><pubDate>Fri, 06 Mar 2026 08:01:00 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!277r!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fddb398ba-9934-4156-91cc-d23366d9b858_527x527.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>You know the feeling. You sit down to tackle an important task, and suddenly you&#8217;re reorganising your desk, checking Slack for the third time in five minutes, or reading articles about productivity instead of actually being productive.</p><p>If you&#8217;re working remotely, this problem has gotten worse. Recent research indicates that <strong>88%</strong> of remote workers procrastinate at least once per week, resulting in a loss of up to <strong>20%</strong> of their productivity. That&#8217;s an entire day each week disappearing into digital distractions and delayed tasks.</p><p>Here&#8217;s what I&#8217;ve learned from recent neuroscience research: procrastination isn&#8217;t a time management problem. It&#8217;s an emotion regulation problem. And once you understand what&#8217;s actually happening in your brain, you can use evidence-based strategies to overcome it.</p><p>Let us examine the science behind procrastination and the practical techniques that actually work.</p><h2>What&#8217;s Actually Happening in Your Brain</h2><p>Groundbreaking research published in Nature Communications in 2024 identified the core neurological mechanism behind procrastination: temporal discounting. Here&#8217;s how it works.</p><p>When you look at a task with a distant deadline or delayed reward, your brain&#8217;s valuation system significantly discounts its value. At the same time, your brain perceives the effort required later as less aversive than doing it now. This creates a cognitive trap: doing something later appears much less effortful but not much less rewarding.</p><p>The result? Your limbic system&#8217;s desire for immediate gratification consistently overrides your prefrontal cortex&#8217;s executive planning functions. It&#8217;s not laziness. It&#8217;s your brain&#8217;s default wiring.</p><p>About 46% of procrastination behaviours have genetic components. This isn&#8217;t a character flaw.</p><p>Researchers Dr Timothy Pychyl from Carleton University and Dr Fuschia Sirois from Durham University have spent years studying procrastination. Their 2024 research confirms what many of us have experienced: we procrastinate to escape negative feelings temporarily. When a task makes us anxious, frustrated, or bored, we postpone it to feel better in the moment.</p><p>The problem is what happens next. We feel guilty about procrastination, which elicits additional negative emotions, which in turn lead to further procrastination. This self-blame cycle causes more damage than the delay itself. Their longitudinal research shows strong correlations between chronic procrastination and severe health outcomes, including coronary heart disease and hypertension.</p><h2>Why Remote Work Makes It Worse</h2><p>If you&#8217;ve noticed yourself procrastinating more since shifting to remote work, you&#8217;re not imagining it. The digital transformation created a perfect storm for procrastination among knowledge workers.</p><p>Four key factors drive higher procrastination rates in remote environments:</p><p><strong>Blurred boundaries.</strong> When your home is your office, work never really ends. But it also never really starts. The clear transition from &#8220;home mode&#8221; to &#8220;work mode&#8221; is absent, making it harder to engage in complex tasks.</p><p><strong>Task ambiguity.</strong> Virtual communication is less clear than in-person conversations. When you&#8217;re not sure exactly what needs to be done or why it matters, your brain labels the task as &#8220;unstructured&#8221; or &#8220;ambiguous.&#8221; These are two of the seven core procrastination triggers.</p><p><strong>Constant escape routes.</strong> Your phone, social media, YouTube, and personal tasks are always one click away. Research shows that continuous connectivity substantially increases exposure to interruptions. Every notification offers an easy escape from whatever uncomfortable task you&#8217;re facing.</p><p><strong>Reduced accountability.</strong> When you&#8217;re working alone, no colleague is dropping by your desk to check progress. Research from Frontiers in Psychology in 2025 found that basic psychological needs for autonomy, competence, and relatedness negatively predict procrastination. Remote environments often fail to meet these needs, particularly the need for relatedness and connection.</p><p>Add decision fatigue to the mix, and you&#8217;ve got a recipe for chronic delay. Every choice you make throughout the day depletes the cognitive resources you need for self-control. By the afternoon, your ability to push through uncomfortable tasks is significantly diminished.</p><h2>What Actually Works: Evidence-Based Strategies</h2><p>A comprehensive meta-analysis of 24 intervention studies involving 1,173 participants reveals what actually reduces procrastination. The answer isn&#8217;t motivation or willpower. It&#8217;s structured approaches that work with your brain, not against it.</p><p>Here&#8217;s what the research supports.</p><h3>Preventive Strategies</h3><p><strong>Implementation intentions. </strong>This is the most powerful technique in the research. Instead of vague goals like &#8220;I&#8217;ll work on the report tomorrow,&#8221; you create specific if-then plans: &#8220;At 9 AM tomorrow at my desk, I will write the report introduction.&#8221;</p><p>This simple shift increases success rates by <strong>300%</strong>. Why? Because it removes the decision point. When 9 AM arrives, you don&#8217;t debate whether to start. You&#8217;ve already decided.</p><p><strong>The Pomodoro Technique.</strong> Work for 25 minutes, then take a 5-minute break. This aligns with how the brain processes cognitive load and supports regular reward cycles. Knowing a break is coming in 25 minutes makes it easier to start complex tasks.</p><p><strong>Task decomposition.</strong> Break projects into sub-2-minute initial steps. Instead of &#8220;write a proposal,&#8221; your first task is &#8220;create a document and write a title.&#8221; This leverages the Zeigarnik Effect, which is your brain&#8217;s preference for completing started tasks. Once you&#8217;ve begun, momentum typically carries you forward.</p><p><strong>Temptation bundling.</strong> Pair aversive tasks with enjoyable activities. Behavioural economist Katherine Milkman developed this approach. Only listen to your favourite podcast while doing expense reports. Only get your premium coffee while working on that difficult presentation. This makes the hard work more appealing.</p><h3><strong>In-the-Moment Tactics</strong></h3><p>When you&#8217;re facing a task right now and feeling the resistance, these techniques help:</p><p><strong>The 5-Minute Miracle.</strong> Commit to working for just five minutes. That&#8217;s it. This bypasses your emotional resistance because five minutes doesn&#8217;t feel threatening. What usually happens? Once you start, you keep going. But even if you don&#8217;t, you&#8217;ve made progress.</p><p><strong>The Swiss Cheese approach.</strong> Make random &#8220;holes&#8221; in large tasks through brief, low-expectation work sessions. Spend 10 minutes just collecting links for your research. Spend 5 minutes outlining section headers. You&#8217;re building progress without the pressure of completing anything.</p><p><strong>Trigger reversal.</strong> Research identifies seven procrastination triggers: boredom, frustration, difficulty, unstructuredness, ambiguity, personal meaninglessness, and a lack of intrinsic rewards. Identify which triggers are activated for your specific task, then deliberately reverse them.</p><p>If a task is tedious, can you make it a game or competition with yourself? If it&#8217;s ambiguous, can you get clarity from someone before starting? If it feels meaningless, can you connect it to a larger goal that matters to you?</p><p><strong>Environmental modification.</strong> Eliminate digital distractions before you start, not while you&#8217;re working&#8212;close unnecessary browser tabs. Put your phone in another room. Use website blockers if needed. Make it more complicated to escape to easy dopamine hits.</p><h3>The Self-Compassion Factor</h3><p>Here&#8217;s something that surprised researchers: self-compassion reduces procrastination more effectively than self-criticism.</p><p>When you procrastinate and then beat yourself up about it, you create more negative emotions. Those emotions drive more procrastination. It&#8217;s a vicious cycle.</p><p>Studies show that people who practice self-forgiveness for past delays are significantly more likely to complete future tasks. When you slip up, acknowledge it without judgment, identify what got in the way, and recommit to your plan.</p><p>Mindfulness-based interventions also show strong results. These improve executive function through body relaxation, breathing practice, and awareness exercises. You don&#8217;t need to become a meditation expert. Even brief mindfulness breaks help restore the self-control required to engage with complex tasks.</p><h2>What You Can Apply Right Now</h2><p>Even if you&#8217;re not struggling with chronic procrastination, here&#8217;s what transfers to any professional situation:</p><p><strong>Start with implementation intentions.</strong> Take your next vital task and create a specific if-then plan. Write down: &#8220;When [specific time and location], I will [specific first action].&#8221; This single technique offers the highest return on investment.</p><p><strong>Design for immediate action, not motivation.</strong> Stop waiting to feel motivated. Motivation follows action, not the other way around. Use the 5-Minute Miracle to get started, and let momentum carry you forward.</p><p><strong>Identify your triggers. </strong>The next time you find yourself procrastinating, ask which of the seven triggers are activated: tedious, frustrating, complex, unstructured, ambiguous, meaningless, or unrewarding. Once you know the trigger, you can address it directly.</p><p><strong>Practice self-compassion.</strong> When you procrastinate, notice it without self-judgment. Understand that your brain is trying to regulate uncomfortable emotions. Acknowledge what happened, identify what you&#8217;ll do differently, and move forward.</p><p><strong>Modify your environment first. </strong>Don&#8217;t rely on willpower to resist digital distractions. Remove them before you start working. Make the right choice, the easy choice.</p><h2>Moving Forward</h2><p>Procrastination isn&#8217;t about laziness or poor character. Your brain attempts to regulate uncomfortable emotions through avoidance. Understanding this changes everything.</p><p>You can&#8217;t eliminate procrastination. However, you can use evidence-based strategies to work with your brain&#8217;s wiring rather than fighting against it. Implementation intentions, task decomposition, the 5-Minute Miracle, trigger reversal, and self-compassion all have strong research support.</p><p>The key is to start small. Pick one technique from this article and try it this week. See what works for your specific situation and constraints.</p><p>Thanks for taking the time to read. If you have questions or if you&#8217;ve found other strategies that work for procrastination, I&#8217;d love to hear about them. Don&#8217;t hesitate to leave comments below.</p>]]></content:encoded></item><item><title><![CDATA[Beyond Patternitis: Why Great Engineers Embrace "The Boring"]]></title><description><![CDATA[In my recent LinkedIn post, I touched on the Pattern-Process Paradox: the growing gap between solving business problems and the ritualistic application of design patterns.]]></description><link>https://vincenyanga.me/p/beyond-patternitis-why-great-engineers</link><guid isPermaLink="false">https://vincenyanga.me/p/beyond-patternitis-why-great-engineers</guid><dc:creator><![CDATA[Vincent Nyanga]]></dc:creator><pubDate>Fri, 20 Feb 2026 08:00:38 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!277r!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fddb398ba-9934-4156-91cc-d23366d9b858_527x527.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In my recent LinkedIn post, I touched on the <strong>Pattern-Process Paradox</strong>: the growing gap between solving business problems and the ritualistic application of design patterns. While patterns were intended to mitigate complexity, their dogmatic over-application often leads to the very unmaintainability they were meant to prevent.</p><p>Today, let&#8217;s go deeper into the <a href="https://arxiv.org/abs/2101.12703">research</a>.</p><h3>The Psychology of the &#8220;Golden Hammer&#8221;</h3><p>To understand why we over-engineer, we must examine how we learn. According to the <strong>Dreyfus model of skill acquisition</strong>, novices and &#8220;advanced beginners&#8221; rely heavily on rigid, context-free rules. For them, a design pattern is a survival mechanism&#8212;a &#8220;black-box&#8221; solution used because they lack the experience to evaluate a problem from first principles.</p><p>The danger zone is the &#8220;Competent&#8221; stage. Here, a developer has learned the <em>how</em> of a pattern but not the <em>when</em>. This is the breeding ground for <strong>Cargo Cult Programming</strong>, in which program structures are treated as rituals rather than functional necessities.</p><h3>The Resume-Driven Development (RDD) Trap</h3><p>We must also acknowledge the market incentives. Research indicates that <strong>82%</strong> of software professionals believe that using emerging technologies makes them more attractive to prospective employers.</p><p>This creates a self-sustaining cycle:</p><ul><li><p><strong>Hiring managers</strong> (60%) admit that tech trends influence their job offerings.</p></li><li><p><strong>Developers</strong> respond by imposing &#8220;buzzword&#8221; architectures, such as 50 microservices for a simple CRUD application, into projects to gain &#8220;marketable&#8221; experience.</p></li></ul><p>The result? A <strong>&#8220;resume-driven legacy&#8221;</strong> of over-engineered systems that are difficult to maintain once the hype for that specific framework fades.</p><p>The Architect&#8217;s Remedy: Strategic Programming</p><p>If the goal of software design is managing complexity, how do we shift back to utility? John Ousterhout&#8217;s <em>A Philosophy of Software Design</em> offers the best framework: <strong>Strategic vs. Tactical Programming</strong>.</p><ol><li><p><strong>Prioritise Deep Modules:</strong> A &#8220;deep&#8221; module hides significant complexity behind a simple interface. Contrast this with &#8220;shallow&#8221; modules, classes, or methods that increase cognitive load by requiring developers to track logic across dozens of fragmented files.</p></li><li><p><strong>Focus on Cohesion:</strong> A deep module thrives on high functional cohesion. Don&#8217;t fragment your logic to satisfy a &#8220;Clean Code&#8221; rule about method length; keep related logic together to reduce obscurity.</p></li><li><p><strong>The 20% Investment:</strong> Tactical programming, which gets the next feature working as quickly as possible, leads to &#8220;Tactical Tornadoes&#8221;. Strategic programming requires an upfront investment of 10-20% of your time in design improvements to ensure the system remains maintainable.</p></li></ol><h3>Final Thought</h3><p>Success in software architecture isn&#8217;t about how many patterns you can fit into a pull request. It&#8217;s about <strong>competence over ritual</strong>. True experts use heuristics and intuition to recognise the &#8220;vibe&#8221; of a failing system and choose the simplest, most effective tool for the job.</p><p>Sometimes, the most &#8220;senior&#8221; thing you can do is choose the boring solution.</p>]]></content:encoded></item><item><title><![CDATA[The Complete Guide to Asynchronous Request-Reply Patterns]]></title><description><![CDATA[Choosing the Right Approach for Your System]]></description><link>https://vincenyanga.me/p/the-complete-guide-to-asynchronous</link><guid isPermaLink="false">https://vincenyanga.me/p/the-complete-guide-to-asynchronous</guid><dc:creator><![CDATA[Vincent Nyanga]]></dc:creator><pubDate>Fri, 06 Feb 2026 08:01:00 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!Jc3G!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa023abf9-5d9b-46b1-a86e-2343d9581b03_674x790.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Your API just returned a 504 Gateway Timeout because generating that report took 45 seconds.</p><p>Your users are frustrated. Your connection pool is exhausted. Your system is brittle.</p><p>The <strong>Asynchronous Request-Reply (ARR) pattern</strong> solves this: acknowledge requests immediately, process in the background, notify when complete.</p><p>Here are your five implementation options and when to use each.</p><h2>1. Polling: Start Here</h2><p><strong>How it works:</strong> The server returns a 202 Accepted response code along with a status URL. Client checks periodically until complete (303 redirect to result).</p><p><strong>Use the </strong><code>Retry-After</code><strong> header.</strong> Let the server control polling frequency&#8212;no guessing needed.</p><p><strong>Best for:</strong> Browser clients, corporate firewalls, tasks under 60 seconds.</p><p><strong>Avoid when:</strong> Tasks take hours, real-time updates are required, or you have thousands of concurrent pollers.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Jc3G!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa023abf9-5d9b-46b1-a86e-2343d9581b03_674x790.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Jc3G!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa023abf9-5d9b-46b1-a86e-2343d9581b03_674x790.png 424w, https://substackcdn.com/image/fetch/$s_!Jc3G!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa023abf9-5d9b-46b1-a86e-2343d9581b03_674x790.png 848w, https://substackcdn.com/image/fetch/$s_!Jc3G!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa023abf9-5d9b-46b1-a86e-2343d9581b03_674x790.png 1272w, https://substackcdn.com/image/fetch/$s_!Jc3G!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa023abf9-5d9b-46b1-a86e-2343d9581b03_674x790.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Jc3G!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa023abf9-5d9b-46b1-a86e-2343d9581b03_674x790.png" width="674" height="790" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a023abf9-5d9b-46b1-a86e-2343d9581b03_674x790.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:790,&quot;width&quot;:674,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:65549,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://vincenyanga.me/i/181017032?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa023abf9-5d9b-46b1-a86e-2343d9581b03_674x790.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Jc3G!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa023abf9-5d9b-46b1-a86e-2343d9581b03_674x790.png 424w, https://substackcdn.com/image/fetch/$s_!Jc3G!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa023abf9-5d9b-46b1-a86e-2343d9581b03_674x790.png 848w, https://substackcdn.com/image/fetch/$s_!Jc3G!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa023abf9-5d9b-46b1-a86e-2343d9581b03_674x790.png 1272w, https://substackcdn.com/image/fetch/$s_!Jc3G!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa023abf9-5d9b-46b1-a86e-2343d9581b03_674x790.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Polling sequence diagram</figcaption></figure></div><h2>2. Webhooks: Push When Ready</h2><p><strong>How it works:</strong> The client provides a callback URL. Server processes in the background and posts the result to the callback when done.</p><p><strong>Security is mandatory.</strong> Verify requests using HMAC signatures or JWT tokens. Never trust incoming webhook data unquestioningly.</p><p><strong>Implement retry logic</strong> with exponential backoff and dead-letter queues. Make your handlers <strong>idempotent</strong>&#8212;you&#8217;ll deliver webhooks multiple times.</p><p><strong>Best for:</strong> Server-to-server communication, event-driven architectures.</p><p><strong>Avoid when:</strong> Browser clients, behind firewalls, or when debugging complexity is a concern.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!L4Ss!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5b31debc-baf8-4f2c-9151-7e4ee1c54c04_626x470.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!L4Ss!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5b31debc-baf8-4f2c-9151-7e4ee1c54c04_626x470.png 424w, https://substackcdn.com/image/fetch/$s_!L4Ss!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5b31debc-baf8-4f2c-9151-7e4ee1c54c04_626x470.png 848w, https://substackcdn.com/image/fetch/$s_!L4Ss!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5b31debc-baf8-4f2c-9151-7e4ee1c54c04_626x470.png 1272w, https://substackcdn.com/image/fetch/$s_!L4Ss!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5b31debc-baf8-4f2c-9151-7e4ee1c54c04_626x470.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!L4Ss!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5b31debc-baf8-4f2c-9151-7e4ee1c54c04_626x470.png" width="626" height="470" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5b31debc-baf8-4f2c-9151-7e4ee1c54c04_626x470.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:470,&quot;width&quot;:626,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:39598,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://vincenyanga.me/i/181017032?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5b31debc-baf8-4f2c-9151-7e4ee1c54c04_626x470.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!L4Ss!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5b31debc-baf8-4f2c-9151-7e4ee1c54c04_626x470.png 424w, https://substackcdn.com/image/fetch/$s_!L4Ss!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5b31debc-baf8-4f2c-9151-7e4ee1c54c04_626x470.png 848w, https://substackcdn.com/image/fetch/$s_!L4Ss!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5b31debc-baf8-4f2c-9151-7e4ee1c54c04_626x470.png 1272w, https://substackcdn.com/image/fetch/$s_!L4Ss!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5b31debc-baf8-4f2c-9151-7e4ee1c54c04_626x470.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Webooks sequence diagram</figcaption></figure></div><h2>3. Server-Sent Events: The Underrated Option</h2><p><strong>How it works:</strong> The client opens a persistent HTTP connection. Server pushes events through this stream when tasks complete.</p><p><strong>Automatic reconnection is built in.</strong> Browsers handle reconnection and resumption using <code>Last-Event-ID</code> header.</p><p><strong>Text-only format.</strong> JSON works great. Binary data needs base64 or a separate HTTP fetch.</p><p><strong>Best for:</strong> Real-time browser updates and one-way server-to-client communication.</p><p><strong>Avoid when:</strong> bidirectional communication, binary streaming, or support for IE/legacy browsers are required.</p><p><strong>Example:</strong> OpenAI&#8217;s ChatGPT streaming responses.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!-yHb!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faa4451a3-7e30-41b9-bc99-21beaf2ab18d_798x729.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!-yHb!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faa4451a3-7e30-41b9-bc99-21beaf2ab18d_798x729.png 424w, https://substackcdn.com/image/fetch/$s_!-yHb!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faa4451a3-7e30-41b9-bc99-21beaf2ab18d_798x729.png 848w, https://substackcdn.com/image/fetch/$s_!-yHb!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faa4451a3-7e30-41b9-bc99-21beaf2ab18d_798x729.png 1272w, https://substackcdn.com/image/fetch/$s_!-yHb!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faa4451a3-7e30-41b9-bc99-21beaf2ab18d_798x729.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!-yHb!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faa4451a3-7e30-41b9-bc99-21beaf2ab18d_798x729.png" width="798" height="729" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/aa4451a3-7e30-41b9-bc99-21beaf2ab18d_798x729.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:729,&quot;width&quot;:798,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:71430,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://vincenyanga.me/i/181017032?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faa4451a3-7e30-41b9-bc99-21beaf2ab18d_798x729.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!-yHb!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faa4451a3-7e30-41b9-bc99-21beaf2ab18d_798x729.png 424w, https://substackcdn.com/image/fetch/$s_!-yHb!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faa4451a3-7e30-41b9-bc99-21beaf2ab18d_798x729.png 848w, https://substackcdn.com/image/fetch/$s_!-yHb!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faa4451a3-7e30-41b9-bc99-21beaf2ab18d_798x729.png 1272w, https://substackcdn.com/image/fetch/$s_!-yHb!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faa4451a3-7e30-41b9-bc99-21beaf2ab18d_798x729.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Server-sent events sequence diagram</figcaption></figure></div><h2>4. WebSockets: For True Bidirectionality</h2><p><strong>How it works:</strong> Persistent, full-duplex connection. Both the client and the server can send messages at any time.</p><p><strong>Operational complexity is objective.</strong> Heartbeat/ping required. Sticky sessions for load balancing. Stateful connection management.</p><p><strong>Best for:</strong> Chat, collaborative editing, gaming&#8212;anything requiring frequent bidirectional updates and sub-100ms latency.</p><p><strong>Avoid when:</strong> One-way updates (use SSE), infrequent communication (use polling), or simple request-reply.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!xO9d!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d9ee9f7-98dc-480f-b51a-1f9919e359ee_954x740.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!xO9d!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d9ee9f7-98dc-480f-b51a-1f9919e359ee_954x740.png 424w, https://substackcdn.com/image/fetch/$s_!xO9d!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d9ee9f7-98dc-480f-b51a-1f9919e359ee_954x740.png 848w, https://substackcdn.com/image/fetch/$s_!xO9d!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d9ee9f7-98dc-480f-b51a-1f9919e359ee_954x740.png 1272w, https://substackcdn.com/image/fetch/$s_!xO9d!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d9ee9f7-98dc-480f-b51a-1f9919e359ee_954x740.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!xO9d!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d9ee9f7-98dc-480f-b51a-1f9919e359ee_954x740.png" width="954" height="740" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6d9ee9f7-98dc-480f-b51a-1f9919e359ee_954x740.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:740,&quot;width&quot;:954,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:83005,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://vincenyanga.me/i/181017032?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d9ee9f7-98dc-480f-b51a-1f9919e359ee_954x740.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!xO9d!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d9ee9f7-98dc-480f-b51a-1f9919e359ee_954x740.png 424w, https://substackcdn.com/image/fetch/$s_!xO9d!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d9ee9f7-98dc-480f-b51a-1f9919e359ee_954x740.png 848w, https://substackcdn.com/image/fetch/$s_!xO9d!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d9ee9f7-98dc-480f-b51a-1f9919e359ee_954x740.png 1272w, https://substackcdn.com/image/fetch/$s_!xO9d!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d9ee9f7-98dc-480f-b51a-1f9919e359ee_954x740.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">WebSockets sequence diagram</figcaption></figure></div><h2>5. Message Brokers: The Enterprise Backbone</h2><p><strong>How it works:</strong> Client publishes to the broker with <code>correlation_id</code> + <code>reply_to</code> Address. The server consumes, processes, and publishes a reply with the same <code>correlation_id</code>. Client matches responses using the correlation ID.</p><p><strong>Idempotency is mandatory.</strong> At least once, delivery means duplicate messages. Your handlers must handle this safely.</p><p><strong>Monitor dead-letter queues.</strong> Failed messages after max retries go to DLQs&#8212;they&#8217;re your canary for system issues.</p><p><strong>Broker choice:</strong></p><ul><li><p><strong>RabbitMQ:</strong> Low latency, complex routing (&lt; 50K msgs/sec)</p></li><li><p><strong>Kafka:</strong> High throughput, event streaming (millions msgs/sec)</p></li><li><p><strong>AWS SQS/SNS:</strong> Managed, serverless, pay-per-use</p></li></ul><p><strong>Best for:</strong> Microservices, guaranteed delivery, high throughput, complex routing.</p><p><strong>Avoid when:</strong> Browser clients, simple APIs, sub-10ms latency needs, and no DevOps expertise.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!9_5a!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f1b67ee-9429-468b-9b5c-55ca5b6978f3_998x682.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!9_5a!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f1b67ee-9429-468b-9b5c-55ca5b6978f3_998x682.png 424w, https://substackcdn.com/image/fetch/$s_!9_5a!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f1b67ee-9429-468b-9b5c-55ca5b6978f3_998x682.png 848w, https://substackcdn.com/image/fetch/$s_!9_5a!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f1b67ee-9429-468b-9b5c-55ca5b6978f3_998x682.png 1272w, https://substackcdn.com/image/fetch/$s_!9_5a!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f1b67ee-9429-468b-9b5c-55ca5b6978f3_998x682.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!9_5a!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f1b67ee-9429-468b-9b5c-55ca5b6978f3_998x682.png" width="998" height="682" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3f1b67ee-9429-468b-9b5c-55ca5b6978f3_998x682.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:682,&quot;width&quot;:998,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:73534,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://vincenyanga.me/i/181017032?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f1b67ee-9429-468b-9b5c-55ca5b6978f3_998x682.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!9_5a!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f1b67ee-9429-468b-9b5c-55ca5b6978f3_998x682.png 424w, https://substackcdn.com/image/fetch/$s_!9_5a!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f1b67ee-9429-468b-9b5c-55ca5b6978f3_998x682.png 848w, https://substackcdn.com/image/fetch/$s_!9_5a!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f1b67ee-9429-468b-9b5c-55ca5b6978f3_998x682.png 1272w, https://substackcdn.com/image/fetch/$s_!9_5a!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f1b67ee-9429-468b-9b5c-55ca5b6978f3_998x682.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Message brokers sequence diagram</figcaption></figure></div><h2>Quick Selection Guide</h2><p><strong>Polling:</strong> Browser clients, most straightforward implementation, tasks &lt; 60 seconds, moderate scale</p><p><strong>Webhooks:</strong> Server-to-server, event-driven, large-scale, need push notifications</p><p><strong>SSE:</strong> Browser real-time updates, one-way communication, simpler than WebSockets</p><p><strong>WebSockets:</strong> Bidirectional, &lt; 100ms latency, chat/collaboration, very large scale</p><p><strong>Message Brokers:</strong> Microservices, millions of messages/second, guaranteed delivery, complex routing</p><h2>Start Simple, Scale Smart</h2><p>Begin with polling. It&#8217;s universally compatible, easy to debug, and solves 80% of async cases.</p><p>Add complexity (WebSockets, message brokers) only when requirements demand it.</p><p>The best architecture solves your actual problems without introducing unnecessary complexity.</p>]]></content:encoded></item><item><title><![CDATA[Ship fast, die slow]]></title><description><![CDATA[Tech debt doesn't kill products. It just bleeds you dry.]]></description><link>https://vincenyanga.me/p/ship-fast-die-slow</link><guid isPermaLink="false">https://vincenyanga.me/p/ship-fast-die-slow</guid><dc:creator><![CDATA[Vincent Nyanga]]></dc:creator><pubDate>Fri, 23 Jan 2026 08:00:50 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!dso_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c8215b8-e113-4d62-ad28-defed3ce93cd_1484x883.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>You&#8217;re shipping fast and making money. Everything works. The codebase is a mess, but who cares? Customers are paying. Features are landing. You&#8217;re winning.</p><p>Then you&#8217;re not.</p><h2>The Slow Death</h2><p>It starts small. A feature that should take a day takes a week. A bug fix that breaks something else. Then something else. Your best engineer, the one who actually understands how things work, quits. They&#8217;re tired of fighting the codebase every day.</p><p>New hires take months to contribute anything meaningful. They&#8217;re not slow. They&#8217;re just drowning in complexity nobody bothered to manage.</p><p>You&#8217;re not slow because the market changed or because your team isn&#8217;t good enough. You&#8217;re slow because every change is now a negotiation with the mess you left behind.</p><h2>The Trap</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!dso_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c8215b8-e113-4d62-ad28-defed3ce93cd_1484x883.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!dso_!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c8215b8-e113-4d62-ad28-defed3ce93cd_1484x883.png 424w, https://substackcdn.com/image/fetch/$s_!dso_!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c8215b8-e113-4d62-ad28-defed3ce93cd_1484x883.png 848w, https://substackcdn.com/image/fetch/$s_!dso_!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c8215b8-e113-4d62-ad28-defed3ce93cd_1484x883.png 1272w, https://substackcdn.com/image/fetch/$s_!dso_!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c8215b8-e113-4d62-ad28-defed3ce93cd_1484x883.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!dso_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c8215b8-e113-4d62-ad28-defed3ce93cd_1484x883.png" width="1456" height="866" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9c8215b8-e113-4d62-ad28-defed3ce93cd_1484x883.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:866,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:88078,&quot;alt&quot;:&quot;Graph showing the relationship between tech debt and speed of delivery. As tech debt accumulates, delivery speed drops exponentially. Three zones are highlighted: green zone where you're shipping fast and everything works, orange zone where features slow down and bugs creep in, and red zone indicating rewrite territory where competitors win. A marker shows 'the moment you notice the pain' &#8212; already halfway into the decline.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://vincentnyanga.substack.com/i/180573253?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c8215b8-e113-4d62-ad28-defed3ce93cd_1484x883.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Graph showing the relationship between tech debt and speed of delivery. As tech debt accumulates, delivery speed drops exponentially. Three zones are highlighted: green zone where you're shipping fast and everything works, orange zone where features slow down and bugs creep in, and red zone indicating rewrite territory where competitors win. A marker shows 'the moment you notice the pain' &#8212; already halfway into the decline." title="Graph showing the relationship between tech debt and speed of delivery. As tech debt accumulates, delivery speed drops exponentially. Three zones are highlighted: green zone where you're shipping fast and everything works, orange zone where features slow down and bugs creep in, and red zone indicating rewrite territory where competitors win. A marker shows 'the moment you notice the pain' &#8212; already halfway into the decline." srcset="https://substackcdn.com/image/fetch/$s_!dso_!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c8215b8-e113-4d62-ad28-defed3ce93cd_1484x883.png 424w, https://substackcdn.com/image/fetch/$s_!dso_!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c8215b8-e113-4d62-ad28-defed3ce93cd_1484x883.png 848w, https://substackcdn.com/image/fetch/$s_!dso_!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c8215b8-e113-4d62-ad28-defed3ce93cd_1484x883.png 1272w, https://substackcdn.com/image/fetch/$s_!dso_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c8215b8-e113-4d62-ad28-defed3ce93cd_1484x883.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">The tech debt curve</figcaption></figure></div><p>Here&#8217;s what makes tech debt dangerous: it doesn&#8217;t announce itself.</p><p>The code keeps working. The product keeps making money. There&#8217;s no alarm, no dashboard turning red, just a gradual erosion of your ability to move.</p><p>&#8220;Move fast and break things&#8221; quietly becomes &#8220;move slow and break everything.&#8221;</p><p>By the time you feel the pain, really feel it, you&#8217;re looking at a six-month rewrite. Your competitors shipped three features while you were untangling spaghetti. The velocity you thought you were protecting by cutting corners? Gone.</p><p>The trap isn&#8217;t that messy code stops working. It&#8217;s that it keeps working just long enough for you to build your entire business on top of it.</p><h2>The Balance</h2><p>I&#8217;m not arguing for clean code as an end in itself. Elegant abstractions don&#8217;t pay the bills. Shipping does.</p><p>But there&#8217;s a difference between &#8220;clean enough to change&#8221; and &#8220;clean enough to frame.&#8221; The goal isn&#8217;t beautiful code. The goal is code you can touch without fear.</p><p>Every shortcut is a bet that you won&#8217;t need to change that code later. Sometimes that bet pays off. Most of the time, you lose, you don&#8217;t know it yet.</p><h2>The Bottom Line</h2><p>The code that makes you money today will need to change tomorrow. New feature. New integration. New regulation. Pivot.</p><p>If you can&#8217;t change it, you can&#8217;t compete. Simple as that.</p><p>So ship fast &#8212; but ship code you can live with because you will be living with it.</p><p>The interest on tech debt compounds silently. And the bill always comes due.</p>]]></content:encoded></item><item><title><![CDATA[Hosting a BFF on AWS: A Simple Guide]]></title><description><![CDATA[CloudFront, S3, Fargate &#8212; one domain, zero tokens in the browser]]></description><link>https://vincenyanga.me/p/hosting-a-bff-on-aws-a-simple-guide</link><guid isPermaLink="false">https://vincenyanga.me/p/hosting-a-bff-on-aws-a-simple-guide</guid><dc:creator><![CDATA[Vincent Nyanga]]></dc:creator><pubDate>Fri, 09 Jan 2026 08:00:59 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!nRGT!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F82192b13-ec5f-4180-bb2d-14b291884e5b_1233x595.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In my previous article I covered the Backend For Frontend (BFF) pattern &#8212; why SPAs shouldn&#8217;t handle OAuth tokens directly. This week: how to actually deploy it on AWS.</p><h2>The Goal</h2><p>One domain. Static frontend and BFF backend. Internal APIs completely hidden from the internet.</p><h2>The Architecture</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!nRGT!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F82192b13-ec5f-4180-bb2d-14b291884e5b_1233x595.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!nRGT!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F82192b13-ec5f-4180-bb2d-14b291884e5b_1233x595.png 424w, https://substackcdn.com/image/fetch/$s_!nRGT!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F82192b13-ec5f-4180-bb2d-14b291884e5b_1233x595.png 848w, https://substackcdn.com/image/fetch/$s_!nRGT!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F82192b13-ec5f-4180-bb2d-14b291884e5b_1233x595.png 1272w, https://substackcdn.com/image/fetch/$s_!nRGT!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F82192b13-ec5f-4180-bb2d-14b291884e5b_1233x595.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!nRGT!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F82192b13-ec5f-4180-bb2d-14b291884e5b_1233x595.png" width="1233" height="595" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/82192b13-ec5f-4180-bb2d-14b291884e5b_1233x595.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:595,&quot;width&quot;:1233,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:51031,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://vincentnyanga.substack.com/i/180472581?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F82192b13-ec5f-4180-bb2d-14b291884e5b_1233x595.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!nRGT!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F82192b13-ec5f-4180-bb2d-14b291884e5b_1233x595.png 424w, https://substackcdn.com/image/fetch/$s_!nRGT!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F82192b13-ec5f-4180-bb2d-14b291884e5b_1233x595.png 848w, https://substackcdn.com/image/fetch/$s_!nRGT!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F82192b13-ec5f-4180-bb2d-14b291884e5b_1233x595.png 1272w, https://substackcdn.com/image/fetch/$s_!nRGT!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F82192b13-ec5f-4180-bb2d-14b291884e5b_1233x595.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Architecture diagram for hosting BFF on AWS</figcaption></figure></div><h2>How It Works</h2><p><strong>CloudFront</strong> is your single entry point. It uses &#8220;behaviors&#8221; to route traffic:</p><ul><li><p><code>/*</code> &#8594; S3 (your static frontend)</p></li><li><p><code>api/*</code> &#8594; ALB (your backend)</p></li></ul><p><strong>S3</strong> hosts your built frontend assets. CloudFront caches them at edge locations globally.</p><p><strong>ALB</strong> receives <code>/api/*</code> requests and forwards them to Fargate. It lives inside your VPC.</p><p><strong>Fargate</strong> runs your BFF. This is where OAuth happens, sessions are managed, and requests are proxied to your internal APIs with access tokens attached.</p><p><strong>Redis</strong> stores sessions. The BFF is stateless &#8212; session data lives here so you can scale horizontally.</p><p><strong>Internal APIs</strong> are your actual backend services. They have no public endpoints. Only the BFF can reach them.</p><h2>The Request Flow</h2><ol><li><p>User loads <code>app.example.com</code> &#8594; CloudFront serves static assets from S3</p></li><li><p>App calls <code>app.example.com/api/orders</code> &#8594; CloudFront routes to ALB</p></li><li><p>ALB forwards to Fargate (BFF)</p></li><li><p>BFF looks up session in Redis, gets access token</p></li><li><p>BFF calls internal API with token attached</p></li><li><p>Response flows back to browser</p></li></ol><p>The browser only ever sees a session cookie. Tokens stay server-side.</p><h2>Why CloudFront Behaviors?</h2><p>You might think you need an ALB in front of everything to do the routing. You don&#8217;t.</p><p>CloudFront handles it natively, and you get:</p><ul><li><p>Free data transfer from S3</p></li><li><p>Edge caching for static assets</p></li><li><p>DDoS protection included</p></li><li><p>Lower cost than ALB-first architecture</p></li></ul><h2>Key Configuration Details</h2><p><strong>CloudFront behavior paths</strong>: Use <code>api/*</code> not <code>/api/*</code>. No leading slash.</p><p><strong>Restrict ALB access</strong>: Add a custom header in CloudFront (e.g., <code>X-Origin-Verify: secret-value</code>). Configure ALB to reject requests without it. This prevents bypassing CloudFront.</p><p><strong>S3 Origin Access Control (OAC)</strong>: Configure CloudFront with OAC so users can only access static assets through CloudFront, not directly via the S3 URL. This ensures caching is always used and the origin is secured.</p><p><strong>Redis for sessions</strong>: Don&#8217;t store sessions in Fargate memory. When requests hit different tasks, sessions disappear. Always use external session storage.</p><h2>Handling SPA Client-Side Routing</h2><p>If you&#8217;re using React Router, Vue Router, or similar, you&#8217;ll hit a common problem: user refreshes on <code>/dashboard/settings</code> and gets a 404.</p><p>Why? S3 looks for a physical file at <code>/dashboard/settings</code>. It doesn&#8217;t exist. S3 returns 404 before your SPA can handle the route.</p><p>The fix: Configure CloudFront to catch 404 errors from S3 and return <code>/index.html</code> instead. Your SPA loads, reads the URL, and routes correctly.</p><p>In CloudFront, go to Error Pages and create a custom error response:</p><ul><li><p>HTTP Error Code: 404</p></li><li><p>Response Page Path: <code>/index.html</code></p></li><li><p>HTTP Response Code: 200</p></li></ul><h2>Cookie Settings</h2><p>This setup enables the strictest possible cookie configuration because everything is on the same domain.</p><pre><code><code>Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Strict; Path=/api</code></code></pre><p>What each flag does:</p><ul><li><p><code>HttpOnly</code> &#8212; JavaScript can&#8217;t read the cookie. XSS can&#8217;t steal it.</p></li><li><p><code>Secure</code> &#8212; Only sent over HTTPS.</p></li><li><p><code>SameSite=Strict</code> &#8212; Only sent on same-site requests. Strongest CSRF protection.</p></li><li><p><code>Path=/api</code> &#8212; Cookie only sent to BFF endpoints, not with static asset requests.</p></li></ul><p><strong>Why same domain matters</strong>: If your frontend and BFF are on different domains (e.g., <code>app.example.com</code> and <code>api.example.com</code>), you&#8217;re forced to use <code>SameSite=None</code>, which is weaker and increasingly blocked by browsers.</p><p>With CloudFront serving both <code>app.example.com/*</code> and <code>app.example.com/api/*</code>, you&#8217;re same-origin. <code>SameSite=Strict</code> just works.</p><h2>That&#8217;s It</h2><p>This architecture handles most production workloads. It&#8217;s secure by default &#8212; your internal APIs have no public exposure, tokens never reach the browser, and CloudFront gives you edge caching and DDoS protection for free.</p><p>Start here. Add complexity only when you need it.</p>]]></content:encoded></item><item><title><![CDATA[Why you SPA shouldn't handle OAuth tokens]]></title><description><![CDATA[Why Your SPA Shouldn&#8217;t Handle OAuth Tokens]]></description><link>https://vincenyanga.me/p/why-you-spa-shouldnt-handle-oauth</link><guid isPermaLink="false">https://vincenyanga.me/p/why-you-spa-shouldnt-handle-oauth</guid><dc:creator><![CDATA[Vincent Nyanga]]></dc:creator><pubDate>Fri, 26 Dec 2025 07:01:18 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!uyOJ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13a50e07-c469-42b8-93e3-3b0244400b6b_1134x567.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>Why Your SPA Shouldn&#8217;t Handle OAuth Tokens</h1><p>Most OAuth tutorials for SPAs show you how to get an access token and store it in <em>localStorage</em>. The app works. You ship it.</p><h2>The Problem with Browser-Based OAuth Clients</h2><p>When your SPA handles OAuth directly, it acts as a &#8220;public client&#8221; and has no secure way to store credentials. The tokens end up in one of the browser's storage areas: localStorage or <em>sessionStorage</em>. Wherever they land, any JavaScript running on your page can access them.</p><p>This includes:</p><ul><li><p>Malicious scripts from XSS vulnerabilities</p></li><li><p>Compromised third-party libraries</p></li><li><p>Injected code from browser extensions</p></li></ul><p>The <a href="https://datatracker.ietf.org/doc/html/draft-ietf-oauth-browser-based-apps">IETF draft</a> on browser-based applications is explicit: browser-based public clients are &#8220;not recommended for business applications, sensitive applications, and applications that handle personal data.&#8221; A large percentage of applications fall into this space</p><h2>The Attack That Changes Everything</h2><p>You might think: &#8220;I&#8217;ll just use short-lived access tokens and refresh token rotation. Even if tokens get stolen, the damage is limited.&#8221;</p><p>That&#8217;s true for simple theft. But there&#8217;s a more sophisticated attack that bypasses all of these defences.</p><p>An attacker with XSS on your page doesn&#8217;t need to steal your tokens. They can get their own.</p><p>Here&#8217;s how:</p><ol><li><p>Inject a hidden iframe</p></li><li><p>Initiate a silent OAuth flow using the user&#8217;s existing session</p></li><li><p>Extract the authorisation code from the iframe</p></li><li><p>Exchange it for a fresh set of tokens</p></li></ol><p>The attacker now has their own access token and refresh token, utterly independent of yours. Short token lifetimes don&#8217;t help. Refresh token rotation doesn&#8217;t help. PKCE doesn&#8217;t help. DPoP doesn&#8217;t help.</p><p>Why? Because the attacker is running a legitimate OAuth flow. They&#8217;re just doing it from your origin, with your user&#8217;s session.</p><h2>The Backend for Frontend Pattern</h2><p>The BFF pattern takes a fundamentally different approach. Instead of your SPA acting as the OAuth client, a backend component handles all OAuth responsibilities.</p><p>The BFF has three jobs:</p><ol><li><p>Act as a confidential OAuth client (with real credentials)</p></li><li><p>Store tokens server-side, tied to a session</p></li><li><p>Proxy all API calls, attaching the access token before forwarding</p></li></ol><p>Your SPA never sees a token. It only receives an HttpOnly session cookie.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!uyOJ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13a50e07-c469-42b8-93e3-3b0244400b6b_1134x567.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!uyOJ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13a50e07-c469-42b8-93e3-3b0244400b6b_1134x567.png 424w, https://substackcdn.com/image/fetch/$s_!uyOJ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13a50e07-c469-42b8-93e3-3b0244400b6b_1134x567.png 848w, https://substackcdn.com/image/fetch/$s_!uyOJ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13a50e07-c469-42b8-93e3-3b0244400b6b_1134x567.png 1272w, https://substackcdn.com/image/fetch/$s_!uyOJ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13a50e07-c469-42b8-93e3-3b0244400b6b_1134x567.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!uyOJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13a50e07-c469-42b8-93e3-3b0244400b6b_1134x567.png" width="1134" height="567" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/13a50e07-c469-42b8-93e3-3b0244400b6b_1134x567.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:567,&quot;width&quot;:1134,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:46929,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://vincentnyanga.substack.com/i/180473901?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13a50e07-c469-42b8-93e3-3b0244400b6b_1134x567.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!uyOJ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13a50e07-c469-42b8-93e3-3b0244400b6b_1134x567.png 424w, https://substackcdn.com/image/fetch/$s_!uyOJ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13a50e07-c469-42b8-93e3-3b0244400b6b_1134x567.png 848w, https://substackcdn.com/image/fetch/$s_!uyOJ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13a50e07-c469-42b8-93e3-3b0244400b6b_1134x567.png 1272w, https://substackcdn.com/image/fetch/$s_!uyOJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13a50e07-c469-42b8-93e3-3b0244400b6b_1134x567.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Architecture diagram showing the BFF pattern</figcaption></figure></div><p></p><h2>Why This Stops the Attack</h2><p>The silent flow attack fails because the BFF is a confidential client. Even if the attacker obtains an authorisation code, they can&#8217;t exchange it because they don&#8217;t have the client secret.</p><p>With a public client, all four attack scenarios apply: stealing existing tokens, stealing tokens continuously, running a silent flow for new tokens, and proxying requests through the user&#8217;s browser.</p><p>With a BFF, only the last one remains. And that&#8217;s not an OAuth vulnerability; it&#8217;s inherent to all web applications. The attacker can make requests while the user&#8217;s browser is open, but they can&#8217;t exfiltrate credentials for later use.</p><h2>When to Use the BFF Pattern</h2><p>If you&#8217;re building business applications, sensitive applications, or anything that handles personal data, use a backend to handle OAuth.</p><p>In practice, this means:</p><ul><li><p>Financial services</p></li><li><p>Healthcare applications</p></li><li><p>Enterprise software</p></li><li><p>Any app with user data you&#8217;d rather not see in a breach notification</p></li></ul><p>The BFF adds infrastructure complexity. You need a backend component, session storage, and a proxy layer. But this complexity provides security guarantees that browser-based OAuth simply cannot.</p><h2>The Middle Ground</h2><p>The decision of whether to use a BFF is not binary. A pure SPA with browser-based OAuth is at one end of the spectrum, while a BFF is at the other. In between the two, there are other options you can employ. Here is one of them:</p><p><strong>Token-Mediating Backend</strong></p><p>This architecture acts as a &#8220;middle ground&#8221; between a full BFF and a browser-only client. It is lighter-weight than a BFF because it does not require proxying every API request through your server, but it is less secure because the access token is exposed to the browser.</p><p>&#8226; <strong>How it works: </strong>You still use a backend component to handle the OAuth exchange (exchanging the authorisation code for tokens)<strong>,</strong> acting as a confidential client. However, instead of keeping the access token hidden, the backend passes it to the browser application. The browser then uses this token to call resource servers directly.</p><p>&#8226; <strong>Security Properties:</strong></p><p>    &#9702; <strong>Refresh Tokens:</strong> The backend keeps the refresh token and does not expose it to the browser, protecting it from theft via XSS. When the access token expires, the browser requests a new one from the backend.</p><p>    &#9702; <strong>Access Tokens:</strong> The access token is exposed to the browser, making it vulnerable to theft if malicious scripts compromise the application.</p><p>    &#9702; <strong>Hijacking:</strong> Because the access token is exposed, an attacker could steal it to call APIs directly, unlike a pure BFF, where the attacker can only hijack the client session.</p><p>&#8226; <strong>Recommendation:</strong> This pattern is recommended only if the use cases or system requirements prevent the use of a proxying BFF.</p><p></p><h2>Analogy</h2><p>Please think of the <strong>BFF</strong> as a bank teller who keeps the vault key (token) behind the counter; you ask them to perform transactions, and they do it for you. The <strong>Token-Mediating Backend</strong> is like a manager who gets the key from the vault but hands it to you to open the safety deposit box yourself; you have more direct access, but if someone steals the key from you, they can open the box too. The <strong>Browser-Based Client</strong> is like having the key mailed directly to your house; it&#8217;s convenient, but anyone who breaks into your mailbox (browser) gets the key immediately.</p><p></p>]]></content:encoded></item><item><title><![CDATA[The architecture behind a reliable AI-powered system]]></title><description><![CDATA[Lessons from building production AI-powered systems: intent classification, guardrails, caching, and feedback loops]]></description><link>https://vincenyanga.me/p/the-architecture-behind-a-reliable</link><guid isPermaLink="false">https://vincenyanga.me/p/the-architecture-behind-a-reliable</guid><dc:creator><![CDATA[Vincent Nyanga]]></dc:creator><pubDate>Fri, 12 Dec 2025 03:15:54 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!MM0N!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb20bebe-be39-403f-9151-2d8521539413_1902x980.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>For the past couple of months, I&#8217;ve been working on AI-powered systems and chatbots at my workplace. With minimal knowledge of architecting and building such systems, I had to rely on online tutorials and books to try to find the best way to build. For the most part, these didn&#8217;t help much, so I resorted to bulldozing through, figuring things out as I went.</p><p>Below is the architecture I&#8217;ve landed on. It&#8217;s definitely not the only way, and I&#8217;m convinced it&#8217;ll improve over time. It&#8217;s been battle-tested, and so far, it covers the gaps that I found in most tutorials.</p><h2>The full picture</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!MM0N!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb20bebe-be39-403f-9151-2d8521539413_1902x980.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!MM0N!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb20bebe-be39-403f-9151-2d8521539413_1902x980.png 424w, https://substackcdn.com/image/fetch/$s_!MM0N!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb20bebe-be39-403f-9151-2d8521539413_1902x980.png 848w, https://substackcdn.com/image/fetch/$s_!MM0N!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb20bebe-be39-403f-9151-2d8521539413_1902x980.png 1272w, https://substackcdn.com/image/fetch/$s_!MM0N!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb20bebe-be39-403f-9151-2d8521539413_1902x980.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!MM0N!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb20bebe-be39-403f-9151-2d8521539413_1902x980.png" width="1456" height="750" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bb20bebe-be39-403f-9151-2d8521539413_1902x980.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:750,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:141055,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://vincentnyanga.substack.com/i/180253660?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb20bebe-be39-403f-9151-2d8521539413_1902x980.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!MM0N!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb20bebe-be39-403f-9151-2d8521539413_1902x980.png 424w, https://substackcdn.com/image/fetch/$s_!MM0N!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb20bebe-be39-403f-9151-2d8521539413_1902x980.png 848w, https://substackcdn.com/image/fetch/$s_!MM0N!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb20bebe-be39-403f-9151-2d8521539413_1902x980.png 1272w, https://substackcdn.com/image/fetch/$s_!MM0N!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb20bebe-be39-403f-9151-2d8521539413_1902x980.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The query flows through several stages before a response reaches the user. Each stage exists because I learned the hard way what happens when you skip it.</p><p>Let&#8217;s walk through each one.</p><h3>1. Cache</h3><p>The first thing a query hits is the cache. Why? Because everything downstream is expensive. RAG lookups, LLM calls, guardrail checks &#8212; all of it costs time and money.</p><p>This isn&#8217;t just key-value caching. For AI systems, you need semantic caching &#8212; matching queries by meaning, not exact string matches. &#8220;What&#8217;s your refund policy?&#8221; and &#8220;How do I get my money back?&#8221; should hit the same cache entry.</p><p><strong>Implementation notes:</strong></p><ul><li><p>Embed incoming queries and compare against cached query embeddings</p></li><li><p>Set a similarity threshold &#8212; too low, and you return wrong answers, too high, and you rarely hit cache</p></li><li><p>Cache responses with TTL based on how often the underlying data changes. One thing I&#8217;m thinking of doing is deciding intelligently what to cache and what not to.</p></li><li><p>Invalidate aggressively when source data updates </p></li></ul><h3>2. Intent classification</h3><p>Before you do any expensive context retrieval, classify what the user actually wants. This serves two purposes:</p><p><strong>Routing</strong>: Different intents need different handling. A simple FAQ lookup doesn&#8217;t need your most powerful model. A complex analysis might need multiple tool calls. Classify first, then route appropriately.</p><p><strong>Safety</strong>: This is your first line of defence against prohibited actions. If someone&#8217;s trying to extract training data, bypass restrictions, or do something harmful, catch it here before you&#8217;ve spent compute on context retrieval.</p><p>I run a very small model at this stage &#8212; fast enough not to add meaningful latency, but accurate enough to catch the obvious cases. The heavy-duty safety checks come later in the guardrails.</p><p><strong>What intent classification catches:</strong></p><ul><li><p>Query type (question, clarification, feedback)</p></li><li><p>Domain routing (which knowledge base or tool set applies)</p></li><li><p>Risk signals (prompt injection attempts, out-of-scope requests)</p></li></ul><h3>3. Contex engineering</h3><p>This is where your RAG pipeline, memory systems, and query rewriting live. The goal: construct the context that gives the model the best chance of generating a helpful response. The user&#8217;s intent from the previous step informs what information needs to be added to the context.</p><p><strong>Query rewriting</strong>: User queries are often ambiguous or incomplete. &#8220;What about the deadline?&#8221; means nothing without context. Rewrite queries to be self-contained using conversation history.</p><p><strong>RAG retrieval</strong>: Pull relevant documents, generate and run database queries. Retrieve what&#8217;s actually relevant based.</p><p><strong>Memory</strong>: This includes both short-term and long-term memory. For multi-turn conversations or returning users, inject relevant history. What did they ask before? What preferences have they expressed? This is especially important for personalisation. In a natural language-to-SQL (nl2sql) system I built, I use data from long-term memory for few-shot prompting. Based on users&#8217; feedback from previous interactions, I&#8217;d add a few examples of <em>good </em>queries, as well as <em>bad </em>queries and reasons why they are good or bad.</p><p><strong>The key insight</strong>: Context engineering is where most of your system's &#8220;intelligence&#8221; comes from. A mediocre model with great context beats a great model with poor context.</p><h3>4. Input guardrails</h3><p>Now the query has context; before it goes to the model, run it through input guardrails. One of the primary concerns here: privacy</p><p>If your context includes personally identifiable information (PII) or any other sensitive data, you need to redact it before sending it to the LLM. This is non-negotiable for most production systems.</p><p><strong>How I handle PII:</strong></p><ul><li><p>Named entity recognition to identify PII</p></li><li><p>Replace with deteministic placeholders (<em>[CUSTOMER_&lt;hash&gt;], [EMAIL_&lt;hash&gt;]</em>, etc.)</p></li><li><p>Store a mapping in the conversation scope so you can restore the original values before sending a response to the user.</p></li><li><p>The mapping never leaves your system &#8212; only the redacted text goes to the model.</p></li></ul><p>Input guardrails also catch anything the intent classifier missed. If a carefully crafted prompt injection made it past classification, this is your second chance to see it.</p><h3>5. Model agnostic router</h3><p>Not every query needs your most expensive model. The router decides where to send the request based on:</p><ul><li><p><strong>Complexity</strong>: Simple lookups go to faster, cheaper models. Complex reasoning goes to more capable ones.</p></li><li><p><strong>Intent</strong>: Code generation might route to a model fine-tuned for code. Creative writing, tuned for that.</p></li><li><p><strong>Cost constraints</strong>: If you have token budgets, the router enforces them.</p></li></ul><p>I call this &#8220;model agnostic&#8221; because the rest of the system doesn&#8217;t care which model handles the request. The router abstracts that decision away.</p><p><strong>Practical tip</strong>: Start with a single model and add routing later. Premature optimisation here adds complexity without proven benefit. Use a router when you have data showing that different queries need different handling.</p><h3>6. Output guardrails</h3><p>The model has generated a response. Before it reaches the user, verify it.</p><p><strong>Factual grounding</strong>: If the response makes claims, can you trace them back to the retrieved context? Flag or filter responses that hallucinate beyond what the context supports.</p><p><strong>Safety checks</strong>: Run the response through content classifiers. Does it contain anything harmful, inappropriate, or policy-violating? Catch it here.</p><p><strong>Format validation</strong>: If you expected structured output, validate it. Malformed responses should retry or fallback, not reach the user.</p><p><strong>Consistency checks</strong>: Does the response contradict earlier statements in the conversation? Does it make promises your system can&#8217;t keep?</p><p>A very small model can be used here to minimise latency.</p><h3>7. Response formatting</h3><p>Final stage: prepare the response that will be sent to the user.</p><p><strong>PII restoration</strong>: Remember those placeholders? Replace them with the original values from your mapping. The user sees real names and data; the model only ever saw redacted versions.</p><p>Then cache the response (if appropriate) and return to the user.</p><h2>The Feedback Loop: Learn and Improve</h2><p>One piece I haven&#8217;t mentioned: the feedback loop.</p><p>User feedback &#8212; explicit (thumbs up/down, ratings) and implicit (follow-up questions, task completion) &#8212; flows back into your system. This informs:</p><ul><li><p><strong>Cache invalidation</strong>: If users consistently dislike a cached response, invalidate it.</p></li><li><p><strong>Memory updates</strong>: Store what worked and what didn&#8217;t for future context</p></li><li><p><strong>Retrieval tuning</strong>: If certain documents consistently lead to inadequate responses, adjust their ranking</p></li><li><p><strong>Model routing</strong>: If one model consistently performs better for certain query types, update routing rules</p></li></ul><p>This is how your system gets smarter over time without retraining models.</p><h2>Principles Behind the Architecture</h2><p>A few principles that shaped these decisions:</p><p><strong>Fail early, fail cheap</strong>: Catch problems as early as possible in the pipeline. Intent classification and cache checks are cheap. Model inference is expensive. Don&#8217;t spend the expensive compute on queries you&#8217;re going to reject anyway.</p><p><strong>Defence in depth</strong>: Don&#8217;t rely on any single safety mechanism. Intent classification, input guardrails, and output guardrails all catch different things. Overlap is fine &#8212; missing something isn&#8217;t.</p><p><strong>Separate concerns</strong>: Each stage has one job. Context engineering doesn&#8217;t know about caching. Guardrails don&#8217;t know about routing. This makes the system testable and maintainable.</p><p><strong>Make it observable</strong>: Every stage should emit metrics such as cache hit rates, guardrail trigger rates, model latencies, and feedback signals. You can&#8217;t improve what you can&#8217;t measure.</p><h2>What I&#8217;d Do Differently</h2><p>If I were starting over:</p><ul><li><p><strong>Add semantic caching earlier</strong>. I underestimated how much duplicate work we were doing.</p></li><li><p><strong>Invest more in intent classification</strong>. A good classifier up front saves so much complexity downstream.</p></li><li><p><strong>Build the feedback loop from day one</strong>. Feedback makes the system self-improve over time.</p></li></ul><h2>Wrapping Up</h2><p>This architecture isn&#8217;t perfect, and it&#8217;s certainly not the only way to build production AI-powered systems. But it handles the problems I kept running into: sensitive data, expensive inference, unreliable outputs, and the need to improve over time.</p><p>The pattern applies whether you&#8217;re building a customer support bot, a document analysis tool, or an internal knowledge assistant &#8212; the specific implementations change; the stages don&#8217;t.</p><p>If you&#8217;re building something similar, I&#8217;d love to hear what&#8217;s working for you. What am I missing? What would you do differently?</p>]]></content:encoded></item><item><title><![CDATA[Utilising Bloom filters in high perfomance system design]]></title><description><![CDATA[Bloom filters have emerged as an elegant and robust solution for data-efficient querying and storage in modern system design.]]></description><link>https://vincenyanga.me/p/utilising-bloom-filters-in-high-perfomance</link><guid isPermaLink="false">https://vincenyanga.me/p/utilising-bloom-filters-in-high-perfomance</guid><dc:creator><![CDATA[Vincent Nyanga]]></dc:creator><pubDate>Fri, 05 Dec 2025 04:23:51 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!277r!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fddb398ba-9934-4156-91cc-d23366d9b858_527x527.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Bloom filters have emerged as an elegant and robust solution for data-efficient querying and storage in modern system design. As a space-efficient probabilistic data structure, a Bloom filter is used to test whether an element is a member of a set. While they allow for a low, tunable rate of false positives, they guarantee no false negatives, meaning a query returns either &#8220;possibly in the set&#8221; or &#8220;definitely not in the set&#8221;. This trade-off makes them indispensable in scenarios where speed and memory optimisation are critical.</p><h2>How Bloom Filters Work</h2><p>A Bloom filter functions using a simple, yet ingenious, structure consisting of a fixed-size bit array (initialised to zeros) and several independent hash functions.</p><ol><li><p><strong>Insertion</strong>: To add an element, it is processed by <em><strong>k </strong></em>hash functions. Each function produces a unique index in the array, and the corresponding bits at these positions are set to 1.</p></li><li><p><strong>Membership query</strong>: To check for membership, the element is hashed again using the same <em><strong>k</strong></em> functions. If <em>all</em> of the corresponding bits are set to 1, the element may be present. If <em>any</em> bit is 0, the element is definitely not in the set.</p></li></ol><p>The memory usage of a Bloom filter remains relatively low because it stores only the hashed representation of the items, not the items themselves. For instance, a filter targeting a 1% false positive probability requires less than 10 bits per element, regardless of the size or number of elements being stored.</p><h2>Key Advantages</h2><p>Bloom filters provide several advantages crucial for scaling modern applications:</p><ul><li><p><strong>Speed and Efficiency</strong>: Both lookups and insertions are speedy, with constant-time complexity <em><strong>O(k)</strong></em>, where <em><strong>k</strong></em> is the number of hash functions. This fixed execution time is independent of the total number of items stored in the set.</p></li><li><p><strong>Memory Efficiency</strong>: Bloom filters excel at representing large sets with a minimal memory footprint, a critical factor for managing infrastructure and running costs, especially where DRAM is involved.</p></li><li><p><strong>Scalability</strong>: Due to their low memory overhead and constant query time, Bloom filters can efficiently handle massive datasets.</p></li><li><p><strong>Privacy Preservation</strong>: They can be used in scenarios like financial fraud detection, allowing organisations to exchange lists (e.g., stolen credit card numbers) to check for matches without revealing the underlying sensitive data</p></li></ul><h2>Core Tradeoffs and Limitations</h2><ul><li><p><strong>False positive rate</strong>: The probability of false positives increases as more elements are added, until all bits are set to 1, at which point all queries will return positive. However, by carefully choosing the bit array size (<strong>m</strong>) and the number of hash functions (<strong>k</strong>), this probability can be controlled. The false positive rate of a Bloom filter can be calculated using the following formula:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;p \\approx \\left(1 - e^{-\\frac{k \\times n}{m}}\\right)^k&quot;,&quot;id&quot;:&quot;BEDXTJVPHJ&quot;}" data-component-name="LatexBlockToDOM"></div><p> Where:</p><ul><li><p><strong>p</strong> is the false positive rate</p></li><li><p><strong>n</strong> is the number of elements in the filter</p></li><li><p><strong>m</strong> is the size of the bit array</p></li><li><p><strong>k</strong> is the number of hash functions</p></li></ul></li><li><p><strong>Inability to delete elements</strong>: Removing an element would require resetting corresponding bits, which could inadvertently affect other elements that share those bits, potentially introducing forbidden false negatives. This makes standard Bloom filters unsuitable for highly dynamic datasets that require frequent removals, though variants such as Counting Bloom Filters address this complexity.</p></li></ul><h2>System Design Applications</h2><p>Bloom filters are widely implemented in systems where offloading expensive checks is paramount:</p><ul><li><p><strong>Databases and Key-Value Stores</strong>: Log-Structured-Merge trees (LSM-trees), used in key-value stores like Cassandra, make use of Bloom filters. Filters are associated with sorted runs of data. During a point lookup, probabilistically consulting the Bloom filter allows the system to skip accessing the run on secondary storage (I/O) if the key is definitely not present.</p></li><li><p><strong>Caching and CDNs</strong>: Bloom filters prevent &#8220;one-hit-wonders&#8221; (data requested only once) from being written to disk, reducing disk I/O and saving valuable cache space. They are also used in web servers to check whether an item is in the cache quickly.</p></li><li><p><strong>Security and Filtering</strong>: Previously, Google Chrome used a local Bloom filter copy of malicious URLs.Only if the filter returned a positive result (a probable hit) would a full, costly check against a server be performed, significantly reducing workload on the centralised malicious URL API.</p></li><li><p><strong>User Management</strong>: Provides a fast, efficient initial check to see whether a desired username has already been used, reducing the number of queries to the central database.</p></li></ul><h2>Optimal Sizing Guide</h2><p>For a desired false positive probability <em>&#1013;</em> and <em>n</em> inserted elements, the memory utilisation is minimised when:</p><ul><li><p>Optimal number of hash functions:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;k \\approx \\frac{m}{n} \\ln 2 &quot;,&quot;id&quot;:&quot;FYDOSYNFQM&quot;}" data-component-name="LatexBlockToDOM"></div></li><li><p>Required bits per element:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\frac{m}{n} \\approx -2.08 \\ln(\\epsilon)&quot;,&quot;id&quot;:&quot;XGBLCSPNUI&quot;}" data-component-name="LatexBlockToDOM"></div><p>For comparison: A 1% error rate (<em>&#1013;</em>=0.01) typically requires 7 hash functions and 9.585 bits per item.</p></li></ul>]]></content:encoded></item><item><title><![CDATA[Best Practices, Patterns & Principles vs Context In Software Development]]></title><description><![CDATA[&#8220;You cannot do that!]]></description><link>https://vincenyanga.me/p/best-practices-patterns-and-principles</link><guid isPermaLink="false">https://vincenyanga.me/p/best-practices-patterns-and-principles</guid><dc:creator><![CDATA[Vincent Nyanga]]></dc:creator><pubDate>Sun, 13 Aug 2023 19:12:27 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!277r!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fddb398ba-9934-4156-91cc-d23366d9b858_527x527.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>&#8220;You cannot do that! It violates <em>X</em> principle&#8230;&#8221; I have heard this statement countless times in my career. At first, when I was still young and clueless, I would feel dirty, like I have committed a mortal sin on which the software gods looked away in disgust. As I went on in my career, I began to question such statements. Did I violate a principle? Am I not using <strong>the</strong> best practice? Does this <em>best</em> practice apply to what I am doing? Some of the time the answer to that question is yes, but in most cases I have found it not to be the case. In this very short post I am going to talk about the importance of context in software engineering.</p><h2>The Buzz Words Plague</h2><p>The software engineering world never runs out of buzz words. Everyday there is something cool. A better way to do something &#8212; a <em>best</em> practice. I have witnessed in awe as my fellow professionals (and myself sometimes) flock to the new shiny thing, shunning yesterday&#8217;s <em>best</em> practices for the most relevant. All of a sudden, that which was once a <em>best</em> practice has instantaneously morphed into an <em>anti pattern.</em></p><p>Again, in some cases that is true. With the constant improvements in technology some of the things that we held in high regard are no longer relevant. However, in most cases, we fall in the trap of going wherever the wind is blowing, blindly applying solutions where they don&#8217;t apply. </p><h2><strong>Principles, Patterns And Best Practices</strong></h2><p>What are software development principles? Software development principles are a set of guidelines that help software engineers write quality, maintainable software. They come about naturally as we encounter similar problems or as we repeatedly make the same mistakes. They provide <em>templates </em>or guides to solve recurring problems. I will using principles and patterns interchangeably in this post though I think they are slightly different. </p><p>Take the <strong>D</strong>on&#8217;t <strong>R</strong>epeat <strong>Y</strong>ourself (DRY) principle for instance. It is a result of people getting caught out when all of a sudden they are required to change the same logic that&#8217;s scattered all over their codebase. It&#8217;s definitely a good guideline. But is it the law? Certainly not! </p><p>Best practices on the other hand, are things that I find mostly misused or dare I say, abused, in the software engineering industry. What is a best practice anyway? To me, a best a practice is something that solves a <em>particular</em> problem better than other options (that the person has managed to come up with). I try to avoid the word <em>best </em>because chances are there is a better way of solving that problem. Are best practices bad? Not if they are applied correctly to the problems that suit them. This is where context comes in.</p><h2>The Importance Of Context</h2><p>Like they say, the best answer in software engineering &#8216;is it depends&#8217;. There is an opportunity cost to every decision we make. Everything has a tradeoff. Choosing which <em>best</em> practice or pattern to use should always be made within the context of the problem space. Not all problems are the same. They may appear similar at face value, which usually leads to incorrectly applying a solution that doesn&#8217;t efficiently or effectively solve the problem. The pattern (or best practice) that worked on your previous project won&#8217;t necessarily apply to the next. </p><p>Should you repeat yourself (<em>violating</em> the DRY principle)? Probably not, but if, in your <em>context</em>, you need to do so, please go ahead. I have witnessed two pieces of logic that appear similar initially, diverge as the project grows and requirements change. Context matters. </p><p>I have countless partial projects on GitHub, most of them trying to solve the same problem using whatever the buzz word was at that moment &#8212; Clean Architecture, DDD, microservices, you name it. Most of the time I stopped midway because of pure laziness. However, in some cases I just hit a brick wall when I realised that I was over engineering the solution while trying to follow the best practice. Certainly that best practice/pattern didn&#8217;t fit my problem space very well. </p><h2>Conclusion</h2><p>Am I saying patterns, principles and good practices are a bad thing? No. They are very useful and most of the time help us avoid banging our heads against the wall while try to solve certain problems. However, they should not be applied blindly without taking into consideration the context. <em>Context is king</em>! </p><p>I would like to hear what your opinion is on this topic. Please feel free to leave a comment below. Thanks so much for taking your time to read.</p>]]></content:encoded></item><item><title><![CDATA[I stopped using GitHub Copilot after four months. Here's why]]></title><description><![CDATA[My experience with GitHub Copilot]]></description><link>https://vincenyanga.me/p/i-stopped-using-github-copilot-after</link><guid isPermaLink="false">https://vincenyanga.me/p/i-stopped-using-github-copilot-after</guid><dc:creator><![CDATA[Vincent Nyanga]]></dc:creator><pubDate>Sun, 06 Aug 2023 05:38:27 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!277r!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fddb398ba-9934-4156-91cc-d23366d9b858_527x527.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>GitHub Copilot is an AI tool that assists with code suggestions while you write code. It was trained using thousands upon thousands of lines of code from GitHub&#8217;s public repositories. Earlier this year I signed up for GitHub Copilot and used it for four months. In this article I will talk about experience and why I eventually stopped using it. </p><h2>My setup</h2><p>I installed Copilot on my Jetbrains Rider and Visual Studio Code IDEs. I write mostly C# code so my feedback is based on C#. If you're using other languages like Python (I heard it's very good with Python), you may have a different experience. </p><h2>What I enjoyed </h2><p>When I started using Copilot, I was amazed at how well it performed. I was especially impressed when it comes to writing unit tests. All I needed to do was write the first test to give it an idea of how I structure my tests. After that, it would generate tests for the other scenarios in the system under test without straying too far off the context.</p><p>While writing logic, I'd prompt Copilot to give me suggestions by adding comments to my code. It would provide a couple of suggestions on how to solve the problem. That was impressive!</p><p>There were few occasions when I provided Copilot with a code block and asked it to explain what the code was doing. It also performed very well. </p><h2>Why I stopped using it </h2><p>As time went by, my excitement started to fade. I started to realise that I wasn't quite enjoying working with Copilot notwithstanding the nice features it has. Here's why I eventually decided to stop using it: </p><h3>Reduced productivity</h3><p>I know Copilot is supposed to increase developer productivity but that wasn't entirely the case with me. While it really helped me when it comes to writing tests, I found myself spending more time trying to debug its suggestions in my head to ensure they made sense before I could accept them.</p><h3>The subtle bugs</h3><p>The reason why I eventually started to scrutinise Copilot&#8217;s suggestions was that there were instances when I blindly accepted the suggestions that looked impressive yet they contained subtle bugs. If I didn't have experience with C# I would have just rolled the code without realising it contained bugs. If you want to use Copilot, I think it's best if you have some experience with the programming language. Otherwise you might introduce some bugs into your codebase. </p><h3>It gets in your face sometimes </h3><p>I don't know how to explain this. I found myself having to switch off Copilot at times because it was getting annoying &#128514;. I'd be in the zone writing some code and it would add suggestions that are way off. I found it extremely distracting, especially when I really wanted to focus. </p><h2>Conclusion </h2><p>In this article I spoke about my experience with GitHub Copilot and why I eventually stopped using it. Don't get me wrong, GitHub Copilot is a great tool and it can be very useful. However, it just didn't click with me. I'd suggest you give it a go if you haven't already, and see how it fairs. Thanks for reading. </p><p></p>]]></content:encoded></item><item><title><![CDATA[Using Azure Event Grid In .NET]]></title><description><![CDATA[Azure Event Grid is a remarkable solution for developers working with event-based architectures.]]></description><link>https://vincenyanga.me/p/using-azure-event-grid-in-net</link><guid isPermaLink="false">https://vincenyanga.me/p/using-azure-event-grid-in-net</guid><dc:creator><![CDATA[Vincent Nyanga]]></dc:creator><pubDate>Sat, 22 Jul 2023 07:03:21 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!cNTq!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4b0e6186-26fc-443e-bb84-7414a13dab8e_622x686.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Azure Event Grid is a remarkable solution for developers working with event-based architectures. It plays a pivotal role in managing the routing of events from any source to any destination, for any application. This service can handle events from Azure services and custom events which can be published directly to the service. These events can then be filtered and sent to various recipients, such as built-in handlers or custom web-hooks. In this article, we will delve deeper into the Azure Event Grid and its .NET client library.</p><h2><strong>Azure Event Grid Concepts</strong></h2><p>Azure Event Grid's functionality can be understood through these concepts:</p><ul><li><p><strong>Event:</strong> Describes what happened.</p></li><li><p><strong>Event source:</strong> Specifies where the event took place.</p></li><li><p><strong>Topic:</strong> The endpoint where publishers send events.</p></li><li><p><strong>Event subscription:</strong> The endpoint or built-in mechanism to route events, sometimes to more than one handler. Subscriptions also help handlers to intelligently filter incoming events.</p></li><li><p><strong>Event handlers:</strong>The application or service responding to the event.</p></li></ul><h2><strong>Event Schemas</strong></h2><p>Event Grid supports two schemas for encoding events &#8212; event grid schema and cloud events v1.0 schema. When a topic or domain is created, you need to specify the schema that will be used when publishing events.</p><h3><strong>Event Grid schema</strong></h3><p>This is the default schema selected if you don&#8217;t specify a schema. This is how the Event Grid Schema looks like:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!cNTq!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4b0e6186-26fc-443e-bb84-7414a13dab8e_622x686.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!cNTq!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4b0e6186-26fc-443e-bb84-7414a13dab8e_622x686.png 424w, https://substackcdn.com/image/fetch/$s_!cNTq!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4b0e6186-26fc-443e-bb84-7414a13dab8e_622x686.png 848w, https://substackcdn.com/image/fetch/$s_!cNTq!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4b0e6186-26fc-443e-bb84-7414a13dab8e_622x686.png 1272w, https://substackcdn.com/image/fetch/$s_!cNTq!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4b0e6186-26fc-443e-bb84-7414a13dab8e_622x686.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!cNTq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4b0e6186-26fc-443e-bb84-7414a13dab8e_622x686.png" width="622" height="686" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4b0e6186-26fc-443e-bb84-7414a13dab8e_622x686.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:686,&quot;width&quot;:622,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:65825,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!cNTq!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4b0e6186-26fc-443e-bb84-7414a13dab8e_622x686.png 424w, https://substackcdn.com/image/fetch/$s_!cNTq!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4b0e6186-26fc-443e-bb84-7414a13dab8e_622x686.png 848w, https://substackcdn.com/image/fetch/$s_!cNTq!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4b0e6186-26fc-443e-bb84-7414a13dab8e_622x686.png 1272w, https://substackcdn.com/image/fetch/$s_!cNTq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4b0e6186-26fc-443e-bb84-7414a13dab8e_622x686.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><h3><strong>CloudEvents schema</strong></h3><p>Another option is to use the CloudEvents v1.0 schema. CloudEvents is a Cloud Native Computing Foundation project which produces a specification for describing event data in a common way. Here is how the schema looks like:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!1bT1!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F47618499-c80c-4cc4-9c20-14771c1ccd24_786x588.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!1bT1!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F47618499-c80c-4cc4-9c20-14771c1ccd24_786x588.png 424w, https://substackcdn.com/image/fetch/$s_!1bT1!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F47618499-c80c-4cc4-9c20-14771c1ccd24_786x588.png 848w, https://substackcdn.com/image/fetch/$s_!1bT1!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F47618499-c80c-4cc4-9c20-14771c1ccd24_786x588.png 1272w, https://substackcdn.com/image/fetch/$s_!1bT1!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F47618499-c80c-4cc4-9c20-14771c1ccd24_786x588.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!1bT1!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F47618499-c80c-4cc4-9c20-14771c1ccd24_786x588.png" width="786" height="588" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/47618499-c80c-4cc4-9c20-14771c1ccd24_786x588.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:588,&quot;width&quot;:786,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:54708,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!1bT1!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F47618499-c80c-4cc4-9c20-14771c1ccd24_786x588.png 424w, https://substackcdn.com/image/fetch/$s_!1bT1!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F47618499-c80c-4cc4-9c20-14771c1ccd24_786x588.png 848w, https://substackcdn.com/image/fetch/$s_!1bT1!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F47618499-c80c-4cc4-9c20-14771c1ccd24_786x588.png 1272w, https://substackcdn.com/image/fetch/$s_!1bT1!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F47618499-c80c-4cc4-9c20-14771c1ccd24_786x588.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><h2><strong>Advantages of Azure Event Grid</strong></h2><p>The Azure Event Grid comes with tangible benefits:</p><ul><li><p>It supports native event handling mechanisms in the Azure cloud application, enabling swift connections between data sources and event handlers.</p></li><li><p>It supports both built-in and custom events.</p></li><li><p>It provides intelligent routing with filters and standardises an event schema.</p></li><li><p>It is a highly reliable service with 24 hours retry.</p></li><li><p>It can support millions of events per second.</p></li><li><p>It greatly enhances serverless, ops automation, and integration work.</p></li></ul><h2><strong>Using Azure Event Grid In .NET</strong></h2><p>There is a client library available to .NET developers. The library provides the following functionality:</p><ul><li><p>Publish events to the Event Grid service using the Event Grid Event, Cloud Event, or custom schemas</p></li><li><p>Consume events that have been delivered to event handlers</p></li><li><p>Generate SAS tokens to authenticate the client publishing events to Azure Event Grid topics</p></li></ul><p>To use it in your application, you need to install if from NuGet:</p><p><code>dotnet add package Azure.Messaging.EventGrid</code></p><h3><strong>Publishing Messages</strong></h3><p>The library provides the `EventGridPublisherClient` class that allows you to publish events to a topic or domain. First you need to create a new instance of the client:</p><p><code>var client = new EventGridPublisherClient("&lt;topic-endpoint&gt;", new AzureKeyCredential("&lt;access-key&gt;"));</code></p><p>The example above uses an access key to authenticate the client. If you are going to host your application in Azure, I highly recommend that you authenticate using a managed identity. The `EventGridPublisherClient` also accepts a set of configuring options through `EventGridPublisherClientOptions`. For example, you can specify a custom serializer that will be used to serialize the event data to JSON.</p><p>Once you have authenticated your client, you can start publishing events. Regardless of what schema your topic or domain is configured to use, `EventGridPublisherClient` will be used to publish events to it. Use the `SendEvent` or `SendEventAsync` method for publishing single events, or `SendEvents`/`SendEventsAsync` if you want to publish multiple events:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!dGu1!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F975b4ea7-9872-4aec-972c-50c8a5a25173_1354x1014.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!dGu1!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F975b4ea7-9872-4aec-972c-50c8a5a25173_1354x1014.png 424w, https://substackcdn.com/image/fetch/$s_!dGu1!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F975b4ea7-9872-4aec-972c-50c8a5a25173_1354x1014.png 848w, https://substackcdn.com/image/fetch/$s_!dGu1!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F975b4ea7-9872-4aec-972c-50c8a5a25173_1354x1014.png 1272w, https://substackcdn.com/image/fetch/$s_!dGu1!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F975b4ea7-9872-4aec-972c-50c8a5a25173_1354x1014.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!dGu1!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F975b4ea7-9872-4aec-972c-50c8a5a25173_1354x1014.png" width="1354" height="1014" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/975b4ea7-9872-4aec-972c-50c8a5a25173_1354x1014.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1014,&quot;width&quot;:1354,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:144220,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!dGu1!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F975b4ea7-9872-4aec-972c-50c8a5a25173_1354x1014.png 424w, https://substackcdn.com/image/fetch/$s_!dGu1!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F975b4ea7-9872-4aec-972c-50c8a5a25173_1354x1014.png 848w, https://substackcdn.com/image/fetch/$s_!dGu1!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F975b4ea7-9872-4aec-972c-50c8a5a25173_1354x1014.png 1272w, https://substackcdn.com/image/fetch/$s_!dGu1!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F975b4ea7-9872-4aec-972c-50c8a5a25173_1354x1014.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3><strong>Receiving Events</strong></h3><p>There are several different Azure services that act as event handlers. These include Azure Functions, Logic Apps or your own custom webhooks.</p><blockquote><p><em><strong>Note:</strong> when using webhooks to handle your events, Event Grid requires you to prove ownership of your webhook endpoint before it starts delivering events to that endpoint.</em></p></blockquote><p>Once events are delivered to the event handler, parse the JSON payload into a list of events.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Q7Qe!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe5be23e-ed34-46a2-96bc-c2be74337fc6_1738x1270.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Q7Qe!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe5be23e-ed34-46a2-96bc-c2be74337fc6_1738x1270.png 424w, https://substackcdn.com/image/fetch/$s_!Q7Qe!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe5be23e-ed34-46a2-96bc-c2be74337fc6_1738x1270.png 848w, https://substackcdn.com/image/fetch/$s_!Q7Qe!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe5be23e-ed34-46a2-96bc-c2be74337fc6_1738x1270.png 1272w, https://substackcdn.com/image/fetch/$s_!Q7Qe!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe5be23e-ed34-46a2-96bc-c2be74337fc6_1738x1270.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Q7Qe!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe5be23e-ed34-46a2-96bc-c2be74337fc6_1738x1270.png" width="1456" height="1064" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/fe5be23e-ed34-46a2-96bc-c2be74337fc6_1738x1270.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1064,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:241913,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Q7Qe!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe5be23e-ed34-46a2-96bc-c2be74337fc6_1738x1270.png 424w, https://substackcdn.com/image/fetch/$s_!Q7Qe!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe5be23e-ed34-46a2-96bc-c2be74337fc6_1738x1270.png 848w, https://substackcdn.com/image/fetch/$s_!Q7Qe!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe5be23e-ed34-46a2-96bc-c2be74337fc6_1738x1270.png 1272w, https://substackcdn.com/image/fetch/$s_!Q7Qe!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe5be23e-ed34-46a2-96bc-c2be74337fc6_1738x1270.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><h2><strong>Conclusion</strong></h2><p>In this post I showed you how you can use Azure Event Grid in your .NET applications. I hope you have learned something. If you have a question, comment or suggestion, please feel free to leave it below. Thanks so much for taking your time to read.</p><p></p>]]></content:encoded></item><item><title><![CDATA[Using Azure Service Bus In .NET]]></title><description><![CDATA[Azure Service Bus, as one of the most powerful and flexible messaging services, has become a cornerstone in the creation of highly reliable and scalable applications.]]></description><link>https://vincenyanga.me/p/using-azure-service-bus</link><guid isPermaLink="false">https://vincenyanga.me/p/using-azure-service-bus</guid><dc:creator><![CDATA[Vincent Nyanga]]></dc:creator><pubDate>Fri, 14 Jul 2023 22:00:00 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!277r!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fddb398ba-9934-4156-91cc-d23366d9b858_527x527.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Azure Service Bus, as one of the most powerful and flexible messaging services, has become a cornerstone in the creation of highly reliable and scalable applications. This article aims to provide an in-depth understanding of Azure Service Bus, its implementation using .NET and C#, and how it can be leveraged to facilitate asynchronous messaging patterns between applications.</p><h2>What Is Azure Service Bus</h2><p>Azure Service Bus is a fully managed enterprise message broker with message queues and publish-subscribe topics. Service Bus is used to decouple applications and services from each other, providing the following benefits:</p><ul><li><p>Load-balancing work across competing workers</p></li><li><p>Safely routing and transferring data and control across service and application boundaries</p></li><li><p>Coordinating transactional work that requires a high-degree of reliability</p></li></ul><p>It acts as a message broker, offering message queues and publish-subscribe topics within a namespace. It provides several benefits, such as load balancing, safe routing, and transaction coordination. Data transfer between applications and services is accomplished using messages. These messages, decorated with metadata, can carry any kind of information, including structured data encoded in common formats like JSON, XML, Apache Avro, or plain text.</p><p>Azure Service Bus supports various messaging scenarios:</p><ul><li><p><strong>Messaging:</strong> Facilitates the transfer of business data like sales orders, purchase orders, etc.</p></li><li><p><strong>Decoupling Applications:</strong> Helps enhance application reliability and scalability.</p></li><li><p><strong>Load Balancing:</strong> Allows multiple consumers to read from a queue simultaneously.</p></li><li><p><strong>Topics and Subscriptions:</strong> Enables one-to-many relationships between publishers and subscribers.</p></li><li><p><strong>Transactions:</strong> Supports several operations within the scope of an atomic transaction.</p></li><li><p><strong>Message Sessions:</strong> Provides a mechanism for grouping related messages.</p></li></ul><h2>Azure Service Bus Concepts</h2><p>When working with Azure Service Bus, there are several concepts to be aware of:</p><ul><li><p><strong>Queues:</strong> Messages are sent to and received from queues, which store the messages until the receiving application is ready to process them. Queues are useful in point-to-point communication scenarios. Only one consumer can receive and process a message from a queue.</p></li><li><p><strong>Topics:</strong> Messages can also be sent and received via topics, which are useful in publish/subscribe scenarios. Unlike queues, topics can have multiple subscriptions, each of which can have multiple consumers. Each subscription receives a copy of every message sent to the topic.</p></li><li><p><strong>Subscriptions:</strong> Subscriptions allow consumers to receive a copy of each message sent to a topic. They can have filters and actions to select and modify messages.</p></li><li><p><strong>Namespaces:</strong> A namespace is a container for all messaging components. It can have multiple queues and topics and often serves as an application container. If you want to use Service Bus, you must first create a namespace.</p></li></ul><h2>Using Azure Service Bus In .NET</h2><p>There is a client library for Azure Service Bus that can be used to send and receive messages. The library is available as a NuGet package, and it can be installed using the following command:</p><pre><code>dotnet add package Azure.Messaging.ServiceBus
</code></pre><h3>Creating A Service Bus Client</h3><p>Once you have installed the package you can create a Service Bus client using the following code:</p><pre><code>const string connectionString = "&lt;connection-string&gt;";
var client = new ServiceBusClient(connectionString);
</code></pre><p>If you are going to host your service on Azure, I highly recommend using <a href="https://honesdev.com/using-azure-managed-identity/">Managed Identity</a> to authenticate with Azure Service Bus.</p><h3>Sending Messages</h3><p>In order to send messages, you need to use an instance of the <code>ServiceBusSender</code> class. You can create an instance of this class using the <code>CreateSender</code> method of the <code>ServiceBusClient</code> class:</p><pre><code>var sender = client.CreateSender("&lt;queue-name&gt;");
</code></pre><p>The <code>ServiceBusSender</code> class provides many methods for sending messages, including sending a batch of messages, sending delayed messages etc. For more information, check out the <a href="https://learn.microsoft.com/en-us/dotnet/api/azure.messaging.servicebus.servicebussender?view=azure-dotnet">documentation</a>. The following code shows how to send a single message:</p><pre><code>var message = new ServiceBusMessage("Hello World!");
await sender.SendMessageAsync(message);
</code></pre><blockquote><p>Note: The ServiceBusSender is safe to cache and use for the lifetime of an application or until the ServiceBusClient that it was created by is disposed. Caching the sender is recommended when the application is publishing messages regularly or semi-regularly.</p></blockquote><h3>Receiving Messages</h3><p>In order to receive messages, you need to use an instance of the <code>ServiceBusReceiver</code> class. You can create an instance of this class using the <code>CreateReceiver</code> method of the <code>ServiceBusClient</code> class:</p><pre><code>var receiver = client.CreateReceiver("&lt;queue-name&gt;");
</code></pre><p>To receive messages, you can use the <code>ReceiveMessageAsync</code> method of the <code>ServiceBusReceiver</code> class:</p><pre><code>var message = await receiver.ReceiveMessageAsync();
var body = message.Body.ToString();
</code></pre><h4>Handling Messages</h4><p>Once you are done processing a message, there are options you have available to handle the message:</p><ul><li><p><strong>Complete:</strong> This will remove the message from the queue or subscription. You call the <code>CompleteMessageAsync</code> method of the <code>ServiceBusReceiver</code> class to complete a message.</p></li><li><p><strong>Abandon:</strong> This will abandon the message and make it available to be received again. You call the <code>AbandonMessageAsync</code> method of the <code>ServiceBusReceiver</code> class to abandon a message.</p></li><li><p><strong>Defer:</strong> Deferring a message will prevent it from being received again using the standard receive methods. Instead, you need to use the <code>ReceiveDeferredMessageAsync</code> to receive deferred messages.</p></li><li><p><strong>Dead-letter:</strong> Dead lettering a message moves it to a sub-queue of the original queue, preventing the message from being received again. You need a receiver scoped to the dead letter queue to receive messages from the dead letter queue.</p></li></ul><h4>Service Bus Processor</h4><p>The <code>ServiceBusProcessor</code> class provides an abstraction around a set of ServiceBusReceiver that allows using an event based model for processing received <code>ServiceBusReceivedMessage</code>. It is constructed by calling <code>CreateProcessor(String, ServiceBusProcessorOptions)</code> on the service bus client. Here is an example of how to use it:</p><pre><code>var processor = client.CreateProcessor("&lt;queue-name&gt;");
processor.ProcessMessageAsync += MessageHandler;
processor.ProcessErrorAsync += ErrorHandler;
await processor.StartProcessingAsync();

static async Task MessageHandler(ProcessMessageEventArgs args)
{
    var body = args.Message.Body.ToString();
    await args.CompleteMessageAsync(args.Message);
}

static Task ErrorHandler(ProcessErrorEventArgs args)
{
    Console.WriteLine(args.Exception.ToString());
    return Task.CompletedTask;
}
</code></pre><p>When using the <code>ServiceBusProcessor</code>, you won&#8217;t need to manually fetch messages from the queue. The processor will automatically fetch messages and call the <code>ProcessMessageAsync</code> event handler. The <code>ProcessErrorAsync</code> event handler will be called if there is an error while processing the message.</p><h2>Conclusion</h2><p>In this post, we looked at how to use Azure Service Bus in .NET. We looked at how to create a Service Bus client, how to send and receive messages, and how to use the Service Bus processor. I hope you found this post useful. If you have any questions or comments, please leave a comment below.</p>]]></content:encoded></item><item><title><![CDATA[Using Azure Storage Queues In .NET]]></title><description><![CDATA[Azure Queue Storage is a cloud-based service that allows you to store a vast number of messages that can be accessed via authenticated HTTP or HTTPS calls from any location globally.]]></description><link>https://vincenyanga.me/p/using-azure-storage-queues-in-dotnet</link><guid isPermaLink="false">https://vincenyanga.me/p/using-azure-storage-queues-in-dotnet</guid><dc:creator><![CDATA[Vincent Nyanga]]></dc:creator><pubDate>Fri, 07 Jul 2023 22:00:00 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!277r!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fddb398ba-9934-4156-91cc-d23366d9b858_527x527.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Azure Queue Storage is a cloud-based service that allows you to store a vast number of messages that can be accessed via authenticated HTTP or HTTPS calls from any location globally. Each message in the queue has a maximum size of 64 KB, while the queue itself can store an enormous number of messages &#8211; the capacity of the storage account. In this post, I will show you how to use Azure Storage Queues in .NET. I will show you how to create a queue, add messages to it, and read messages from it.</p><h2>Prerequisites</h2><p>Before you begin, you need to have an Azure subscription. If you don&#8217;t have one, you can create a <a href="https://azure.microsoft.com/en-us/free/">free account</a>. If you don&#8217;t want to create an account, that&#8217;s okay too. You can use Azurite, which is a local storage emulator that simulates the Azure Storage services for testing purposes. You can download it from <a href="https://learn.microsoft.com/en-us/azure/storage/common/storage-use-azurite">here</a>. If you are going to use the real Azure Queue Storage, you need to create a Storage Account. You can find how to do it <a href="https://docs.microsoft.com/en-us/azure/storage/common/storage-account-create?tabs=azure-portal">here</a>.</p><p>Once you have that in place, you need to install the Azure Storage Queue NuGet package. You can do it by running the following command in the Package Manager Console.</p><pre><code>dotnet add package Azure.Storage.Queues
</code></pre><h2>Creating a Queue</h2><p>To create a queue, you need to create an instance of the <code>QueueClient</code> class. You need to pass the connection string or use managed identity, and the name of the queue to the constructor. The connection string can be found in the Azure Portal. You can find it in the Access Keys section of the Storage Account. You can optionally pass in an instance of the <code>QueueClientOptions</code> class to the constructor. The <code>QueueClientOptions</code> class allows you to configure the retry policy, message encoding and other options. Here is how you can create a queue in C#.</p><pre><code>const string connectionString = "your_connection_string";
const string queueName = "your_queue_name";

var queueClient = new QueueClient(connectionString, queueName);

await queueClient.CreateIfNotExistsAsync();
</code></pre><p>The code snippet above is pretty straight forward. You create an instance of the <code>QueueClient</code> class and call the <code>CreateIfNotExistsAsync</code> method. If the queue doesn&#8217;t exist, it will be created. If it exists, nothing will happen.</p><h2>Adding Messages to a Queue</h2><p>Azure Storage Queues support two types of messages: string messages and binary messages. To add a message to a queue, you need to call the <code>SendMessageAsync</code> method on the <code>QueueClient</code> class. The method accepts a <code>string</code> or <code>BinaryData</code> as a message body. You can optionally pass in a time-to-live and a visibility timeout. The time-to-live is the time that the message will be stored in the queue. The visibility timeout is the time that the message will be invisible to other consumers after it has been dequeued. Here is how you can add a message to a queue in C#.</p><pre><code>var message = "Hello, World!";
var timeToLive = TimeSpan.FromMinutes(5);
var visibilityTimeout = TimeSpan.FromSeconds(30);
await queueClient.SendMessageAsync(message, timeToLive, visibilityTimeout);
</code></pre><p>If your message is a complex object, you will need to serialize it before sending it to the queue.</p><blockquote><p><em><strong>NOTE:</strong></em> You need to ensure that your message encoding is the same between the sender and the receiver.</p></blockquote><h2>Reading Messages from a Queue</h2><p>There are two options for reading messages from a queue: peeking and dequeuing. When you peek a message, it will remain in the queue. When you dequeue a message, it will be removed from the queue and other processes can access it. To peek a message, you need to call the <code>PeekMessagesAsync</code> method on the <code>QueueClient</code> class. The method accepts the number of messages to peek. Here is how you can peek a message from a queue in C#.</p><pre><code>var peekedMessages = await queueClient.PeekMessagesAsync(1);
var peekedMessage = peekedMessages.Value.FirstOrDefault();
</code></pre><p>To dequeue a message, you need to call the <code>ReceiveMessagesAsync</code> method on the <code>QueueClient</code> class. The method accepts the number of messages to dequeue. Here is how you can dequeue a message from a queue in C#.</p><pre><code>var dequeuedMessages = await queueClient.ReceiveMessagesAsync(1);
var dequeuedMessage = dequeuedMessages.Value.FirstOrDefault();
</code></pre><p>When you call the <code>ReceiveMessageAsync</code> method, the message(s) will become invisible to other consumers for the visibility timeout period. After the visibility timeout period has elapsed, the message(s) will become visible again. If you are done processing the message and don&#8217;t want it to be visible for other consumers agains, you need to call the <code>DeleteMessageAsync</code> method on the <code>QueueClient</code> class to remove it from the queue. The method accepts the message ID and the pop receipt. Here is how you can delete a message from a queue in C#.</p><pre><code>await queueClient.DeleteMessageAsync(dequeuedMessage.MessageId, dequeuedMessage.PopReceipt);
</code></pre><h2>Demo Application</h2><p>I have a demo application that shows how to use Azure Storage Queues in .NET. You can find it on <a href="https://github.com/vince-nyanga/storage-queue-sample">GitHub</a>. It consists for two console applications, one for sending messages and the other for receiving them. It uses <a href="https://docs.coravel.net/">Coravel</a> to schedule the jobs. Feel free to clone it and play around with it.</p><h2>Conclusion</h2><p>In this post, I showed how to use Azure Storage Queues in .NET. I showed how to create a queue, add messages to it, and read messages from it. I hope you have found this post useful. Storage queues can be very useful when you want to build a decoupled message driven system that doesn&#8217;t cost an arm and a leg. It is important to note that storage queues are very simple and don&#8217;t have advanced features such as message ordering or transactions. I hope you have learned something and thanks so much for reading.f</p>]]></content:encoded></item><item><title><![CDATA[Choosing The Best Azure Messaging Service For Your Application]]></title><description><![CDATA[Taking a look at the various messaging services Azure offers]]></description><link>https://vincenyanga.me/p/choosing-the-best-azure-messaging</link><guid isPermaLink="false">https://vincenyanga.me/p/choosing-the-best-azure-messaging</guid><dc:creator><![CDATA[Vincent Nyanga]]></dc:creator><pubDate>Sat, 01 Jul 2023 16:32:00 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!277r!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fddb398ba-9934-4156-91cc-d23366d9b858_527x527.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When using microservices architecture, you'll likely need to incorporate messaging services to enable decoupling of your services. Azure offers a few messaging services: Azure Event Grid, Azure Event Hubs, Azure Storage queues and Azure Service Bus. Each of these services is designed for specific scenarios, and understanding the differences between them will help you choose the right one for your application. In many cases, these services can be used together to complement each other.</p><h2><strong>Events vs. Messages</strong></h2><p>Before diving into the details of each messaging service, let's clarify the distinction between events and messages. An event is a lightweight notification of a condition or a state change -- something that has already happened. The publisher of the event has no expectation about how the event is handled, and the consumer decides what to do with the notification. Events can be discrete units or part of a series.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://vincenyanga.me/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Vincent Nyanga! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>On the other hand, a message is raw data produced by a service to be consumed or stored elsewhere. The message contains the data that triggered the message pipeline, and there is a contract between the publisher and the consumer on how the message should be handled. In other words, a message is a request for action.</p><p>Now that we have a clear understanding of events and messages, let's explore each Azure messaging service in more detail.</p><h2><strong>Azure Event Grid</strong></h2><p><a href="https://learn.microsoft.com/en-us/azure/event-grid/">Azure Event Grid </a>is an eventing backplane that enables event-driven, reactive programming. It follows the publish-subscribe model, where publishers emit events without any expectations on how they are handled, and subscribers decide which events they want to handle. Event Grid is deeply integrated with Azure services and can also be added to your own applications.</p><p>One of the key benefits of Event Grid is its ability to simplify event consumption and reduce costs by eliminating the need for constant polling. It efficiently and reliably routes events from Azure and non-Azure resources to registered subscriber endpoints. Event Grid provides the necessary information to react to changes in services and applications, but it does not deliver the actual object that was updated.</p><p>Azure Event Grid supports both push and pull models. In the push model, Event Grid delivers (pushes) events to subscribers. With this model, you will need to ensure that your subscribers are able to handle all the messages pushed, otherwise you might need to implement way to handle the backpressure. In the pull model, subscribers pull events from Event Grid at their own pace.</p><p>Event Grid is a great choice when you want to react to discrete events that occur in Azure services or your own applications.</p><h2><strong>Azure Event Hubs</strong></h2><p><a href="https://learn.microsoft.com/en-us/azure/event-hubs/">Azure Event Hubs </a>is a big data streaming platform and event ingestion service. It is designed to handle high-throughput, real-time data streams from various sources. Event Hubs can receive and process millions of events per second, making it an ideal choice for scenarios that require capturing, retaining, and replaying telemetry and event stream data.</p><p>Event Hubs provides low-latency event ingestion and enables the capture of streaming data for further processing and analysis. It can be seamlessly integrated with various stream-processing infrastructures and analytics services. Whether you need real-time processing or repeated replay of stored raw data, Event Hubs offers a scalable solution.</p><p>Event Hubs is a great choice when you need to ingest and process high-volume event streams.</p><h2><strong>Azure Storage Queue</strong></h2><p><a href="https://learn.microsoft.com/en-us/azure/storage/queues/">Azure Storage queue</a> is a simple message queuing service that stores large numbers of messages that can be accessed from anywhere in the world via authenticated calls using HTTP or HTTPS. A queue message can be up to 64 KB in size, and a queue can contain millions of messages, up to the total capacity limit of a storage account.</p><p>Storage queues are very primitive and do not support any advanced features such as message ordering, duplicate detection, or dead-lettering. However, they are very cost-effective and can be used to decouple applications and services. Storage queues are a great choice when you need a simple, reliable, and inexpensive messaging solution.</p><h2><strong>Azure Service Bus</strong></h2><p><a href="https://learn.microsoft.com/en-us/azure/service-bus-messaging/service-bus-messaging-overview">Azure Service Bus</a> is a fully managed enterprise message broker that provides reliable message delivery with support for message queues as well as publish-subscribe topics. It is designed for enterprise applications that require features like transactions, message ordering, duplicate detection, and instantaneous consistency.</p><p>Azure Service Bus acts as a brokered messaging system, storing messages until the consuming party is ready to receive them. It is an ideal choice for scenarios that involve high-value messages that cannot be lost or duplicated. Azure Service Bus also facilitates secure communication across hybrid cloud solutions and enables the connection of on-premises systems to cloud solutions.</p><p>Service Bus is a great choice when you need a fully managed enterprise messaging service with advanced features</p><h2><strong>Choosing the Right Messaging Service</strong></h2><ul><li><p><strong>Azure Event Grid</strong>: Event Grid is best suited for reactive programming scenarios that involve event distribution, allowing you to react to status changes in your services or applications. It is particularly useful for serverless solutions that require scalability and cost efficiency.</p></li><li><p><strong>Azure Event Hubs</strong>: Event Hubs is designed for big data streaming scenarios, where high-throughput, real-time data ingestion is required. It is an excellent choice for capturing and processing telemetry and event stream data from multiple sources.</p></li><li><p><strong>Azure Storage queue</strong>: Storage queues are a simple, reliable, and cost-effective messaging solution. They are a great choice for decoupling applications and services, and they can be used to implement asynchronous processing or reliable communication between components.</p></li><li><p><strong>Azure Service Bus</strong>:Service Bus is the go-to messaging service for enterprise applications that require reliable message delivery, support for transactions, ordering, and advanced messaging features. It provides the necessary infrastructure for seamless communication between cloud and on-premises systems.</p></li></ul><p>While each messaging service has its specific use cases, it's important to note that they can also be used together to fulfil distinct roles or form an event and data pipeline.</p><h2><strong>Conclusion</strong></h2><p>In this post, I introduced some of the messaging services available in Azure and discussed their key features and use cases. I hope this post helps you choose the right messaging service for your application. In the next series of posts, I will show how to use each of these messaging services in .NET. If you have any questions or comments, please leave them in the comments section below. Thanks for reading!</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://vincenyanga.me/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Vincent Nyanga! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item></channel></rss>