<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: John Eliud Odhiambo</title>
    <description>The latest articles on DEV Community by John Eliud Odhiambo (@johneliud).</description>
    <link>https://dev.to/johneliud</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1459938%2F57cf98f3-ee3a-432e-9d06-b280c4f141d8.jpg</url>
      <title>DEV Community: John Eliud Odhiambo</title>
      <link>https://dev.to/johneliud</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/johneliud"/>
    <language>en</language>
    <item>
      <title>Where Were the Logs? Building a Log Ingestion Engine That Never Silently Drops a Log</title>
      <dc:creator>John Eliud Odhiambo</dc:creator>
      <pubDate>Thu, 29 Oct 2026 16:13:18 +0000</pubDate>
      <link>https://dev.to/johneliud/where-were-the-logs-building-a-log-ingestion-engine-that-never-silently-drops-a-log-388b</link>
      <guid>https://dev.to/johneliud/where-were-the-logs-building-a-log-ingestion-engine-that-never-silently-drops-a-log-388b</guid>
      <description>&lt;p&gt;Three hours were lost in this example scenario. Two hours were spent on root cause analysis (where an engineer would log onto each server, open up various log files, and search manually). There was no error reported from any of the applications on the monitoring dashboard, meaning that no panels highlighted the service that was failing. There was also no clear indication of which service was failing the most. The compliance team wanted to see audit trails, but there were none to provide.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://reduzer.ac.ke/projects/log-ingestion-engine" rel="noopener noreferrer"&gt;brief&lt;/a&gt; is the running example I built this project from, and I later published the project as a &lt;a href="https://github.com/johneliud/log_ingestion_engine" rel="noopener noreferrer"&gt;repository&lt;/a&gt;.&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;TL;DR:&lt;/code&gt; Go service which receives batched JSON on &lt;code&gt;/logs&lt;/code&gt;, validates at field level, enriches and routes to three SQLite destinations via RabbitMQ with a bounded in-memory fallback. Admission is durable via a write-ahead spool fsynced before 202; unacked spool entries are replayed on startup. Batch writer with bounded retries and rotating dead-letter file, idempotent writes per &lt;code&gt;log_id&lt;/code&gt;. Load test sustained 1,000 logs/s acceptance with 12 pipeline workers, batch 100, on a laptop; default 4 workers do not sustain the target.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  1. The problem
&lt;/h2&gt;

&lt;p&gt;There are twelve services, each producing bespoke log files onto individual disks. There is no central search, no live dashboard, and no audit trail. It can take an hour to determine which service (or machine), or which file had the issue. By using grep on a laptop, it can be found in seconds. However, using grep on all twelve different formats takes an entire afternoon.&lt;/p&gt;

&lt;p&gt;This resulted in three requirements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A central search of all log files.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Signals in minutes about which service has an issue and how big the issue is.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;An audit trail that is persistent and no data is lost when restarting the service.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The log ingestion engine has been built to provide some of the searchable requirements (see section 7).&lt;/p&gt;




&lt;h2&gt;
  
  
  2. The goal and the ground rules
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Overview
&lt;/h3&gt;

&lt;p&gt;A centralised logging platform to receive logs using an API, validate and enrich log data, route by service, store it reliably, and provide signals so we can see issues occur before our customers do.&lt;/p&gt;

&lt;h4&gt;
  
  
  Constraints
&lt;/h4&gt;

&lt;p&gt;The brief says we must limit our libraries. I made a few changes here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Used the rate-limiting library golang.org/x/time/rate rather than implementing my own token bucket. The interesting engineering was implementing per-IP keys, request shaping, and Retry-After.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The frontend uses React with Vite, Tailwind CSS as a utility CSS framework, Space Grotesk from the Google Fonts CDN, and Chart.js for charts, instead of hand-written HTML/CSS.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Storage is kept within the brief:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Three databases using SQLite.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;One file per destination.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The configuration is simple with only&amp;nbsp;.env files, integer values for duration, and &lt;code&gt;ROUTING_RULES&lt;/code&gt;. The cost of using SQLite on real logs is discussed in section 7.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Architecture at a glance
&lt;/h2&gt;

&lt;p&gt;This diagram represents the architecture shipped with the project:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TB
    client(["Clients"])

    subgraph ingress["HTTP ingress"]
        direction TB
        rateLimit["Rate limiter&amp;lt;br/&amp;gt;per IP, in-memory"]
        logsEndpoint["POST /logs"]
        validate["Validate batch&amp;lt;br/&amp;gt;body size + per-entry errors"]
    end

    spool[("Write-ahead spool&amp;lt;br/&amp;gt;fsync before ack")]
    rawQueue[("raw_logs&amp;lt;br/&amp;gt;bounded channel")]

    subgraph processing["Pipeline worker pool"]
        direction TB
        enrich["Enrich&amp;lt;br/&amp;gt;received_at · source_ip · environment"]
        route["Route&amp;lt;br/&amp;gt;service → destination rules"]
    end

    subgraph publishing["Publisher"]
        direction TB
        rabbitPublisher["RabbitMQ publisher&amp;lt;br/&amp;gt;persistent + confirms&amp;lt;br/&amp;gt;5s publish timeout&amp;lt;br/&amp;gt;auto-reconnect with backoff"]
        fallback[("Bounded in-memory fallback&amp;lt;br/&amp;gt;used while disconnected")]
    end

    broker[("RabbitMQ&amp;lt;br/&amp;gt;exchange_service1..3&amp;lt;br/&amp;gt;queue_service1..3&amp;lt;br/&amp;gt;routing key: log.write")]

    subgraph delivery["Consumers and storage"]
        direction TB
        supervisor["Supervisor&amp;lt;br/&amp;gt;one connection&amp;lt;br/&amp;gt;declares topology, reconnects"]
        amqpConsumer["AMQP consumers&amp;lt;br/&amp;gt;one connection per queue&amp;lt;br/&amp;gt;ack after stored"]
        fallbackConsumer["Fallback consumer"]
        batchWriter["Batch writer&amp;lt;br/&amp;gt;per-destination buffers&amp;lt;br/&amp;gt;retry with backoff"]
        sqlite[("SQLite&amp;lt;br/&amp;gt;logs_service1.db&amp;lt;br/&amp;gt;logs_service2.db&amp;lt;br/&amp;gt;logs_service3.db")]
        deadLetters[("Dead-letter NDJSON&amp;lt;br/&amp;gt;one shared file&amp;lt;br/&amp;gt;rotated by size")]
        replay["cmd/replay"]
    end

    client --&amp;gt; rateLimit --&amp;gt; logsEndpoint --&amp;gt; validate --&amp;gt; spool --&amp;gt; rawQueue
    rawQueue --&amp;gt; enrich --&amp;gt; route --&amp;gt; rabbitPublisher
    rabbitPublisher --&amp;gt;|"connected"| broker
    rabbitPublisher --&amp;gt;|"publish fails"| fallback
    fallback -.-&amp;gt;|"fallback full: dead letter"| deadLetters
    broker --&amp;gt;|"deliver"| supervisor
    supervisor --&amp;gt; amqpConsumer
    fallback --&amp;gt;|"stored directly, never re-published"| fallbackConsumer
    amqpConsumer --&amp;gt; batchWriter
    fallbackConsumer --&amp;gt; batchWriter
    batchWriter --&amp;gt;|"stored"| sqlite
    batchWriter --&amp;gt;|"retries exhausted"| deadLetters
    deadLetters -.-&amp;gt; replay
    replay -.-&amp;gt;|"replay"| sqlite&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;There are 6 stages within the pipeline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Post&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Validate&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Buffer&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Enrich/Route&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Publish/Fallback&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Consume/Store&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    post["1 Post&amp;lt;br/&amp;gt;rate limit,&amp;lt;br/&amp;gt;accept the batch"]
    validate["2 Validate and spool&amp;lt;br/&amp;gt;per-entry errors,&amp;lt;br/&amp;gt;fsync admission"]
    buffer["3 Buffer&amp;lt;br/&amp;gt;bounded raw_logs&amp;lt;br/&amp;gt;channel (10,000)&amp;lt;br/&amp;gt;202 or 503"]
    enrichRoute["4 Enrich and route&amp;lt;br/&amp;gt;stamp fields,&amp;lt;br/&amp;gt;pick destination"]
    publish["5 Publish&amp;lt;br/&amp;gt;RabbitMQ, or the&amp;lt;br/&amp;gt;in-memory fallback"]
    store["6 Consume and store&amp;lt;br/&amp;gt;batched SQLite writes,&amp;lt;br/&amp;gt;dead letters on failure"]

    post --&amp;gt; validate --&amp;gt; buffer --&amp;gt; enrichRoute --&amp;gt; publish --&amp;gt; store&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Each stage handles one specific type of failure/slowdown.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The rate limiter handles abusive clients.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;raw_logs&lt;/code&gt; handles separating quick HTTP traffic from slow enrichment.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The publisher fallback handles outages of the broker. The logs are each written to storage once, as persistence is idempotent on a per &lt;code&gt;log_id&lt;/code&gt; basis. They are never ping-ponged between the broker and memory.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The batch writer spreads the cost of inserting into SQLite.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The dead letter file handles unrecoverable failure.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fallback is consumed by a dedicated consumer and written directly to storage. No re-publishing occurs. As consumers only acknowledge once data has been stored, it provides at-least-once delivery. Persistence within the database is idempotent on a per &lt;code&gt;log_id&lt;/code&gt; basis, so duplicate data can be handled.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. The stages
&lt;/h2&gt;

&lt;h3&gt;
  
  
  4.1 Ingestion and validation
&lt;/h3&gt;

&lt;p&gt;This will validate the Content-Type header, ensure the body is a certain size using http.MaxBytesReader, parse the JSON, and ensure that there are four fields for each log entry. These are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Timestamp (&lt;code&gt;RFC3339&lt;/code&gt;)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Service (at most 100 characters)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Level (&lt;code&gt;INFO/WARN/ERROR/DEBUG&lt;/code&gt;)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Message (at most 10,000 characters)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Validation runs first. Valid entries are appended to the write-ahead spool and fsynced to disk. The handler then attempts to enqueue the batch onto the bounded &lt;code&gt;raw_logs&lt;/code&gt; channel. If the enqueue succeeds within &lt;code&gt;CHANNEL_SEND_TIMEOUT&lt;/code&gt;, the handler returns &lt;code&gt;202&lt;/code&gt; with the batch ID and per-entry rejection details; if the channel is full or the send times out, the handler returns &lt;code&gt;503&lt;/code&gt;. The &lt;code&gt;202&lt;/code&gt; therefore means the batch is durably admitted for replay and now being processed, not yet in SQLite. The spool guarantees the batch can be replayed on restart. In the &lt;code&gt;503&lt;/code&gt; case, the spool entry is retained so it can be replayed later; the client may retry and create duplicates, which receive new &lt;code&gt;log_id&lt;/code&gt;s and are therefore stored as separate rows (idempotency does not collapse them).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design decision:&lt;/strong&gt; I decided to accept partial batches. This will mean that if a log has an invalid time, it will only lose one log instead of one hundred. Each valid log entry will have a UUID added as the &lt;code&gt;log_id&lt;/code&gt; field. This means they can be safely resent and played back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design decision:&lt;/strong&gt; The spool fsync is performed inline on the request path to make a 202 durable. This breaks the “no blocking” ideal and adds latency on the critical path, but it guarantees admission durability. The fsync is small per batch, and the cost is accepted for durability; compaction of the spool log and &lt;code&gt;.acked&lt;/code&gt; file occurs only when every spooled ID has been acknowledged.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;ValidateLogEntries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logEntries&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LogEntry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;validEntries&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LogEntry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rejectedEntries&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RejectedEntry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;entryIndex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;logEntry&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;logEntries&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;fieldErrors&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;validateLogEntry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logEntry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fieldErrors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="n"&gt;rejectedEntries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rejectedEntries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RejectedEntry&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Index&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="n"&gt;entryIndex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Errors&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;fieldErrors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="n"&gt;validEntries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;validEntries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;logEntry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
 &lt;span class="p"&gt;}&lt;/span&gt;
 &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;validEntries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rejectedEntries&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Valid and invalid log entries will be separated by &lt;code&gt;ValidateLogEntries&lt;/code&gt; whilst retaining their original index.&lt;/p&gt;

&lt;p&gt;Every log entry needs to have a time, service, level, and message. &lt;code&gt;POST /logs&lt;/code&gt; can return &lt;code&gt;202&lt;/code&gt;, &lt;code&gt;400&lt;/code&gt;, &lt;code&gt;413&lt;/code&gt;, &lt;code&gt;415&lt;/code&gt;, &lt;code&gt;503&lt;/code&gt;, or a &lt;code&gt;429&lt;/code&gt; from the limiter.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.2 Protecting the boundary
&lt;/h3&gt;

&lt;p&gt;Rate limiting middleware using a lazy in-memory token bucket, keyed on the source IP address. Token buckets are purged explicitly when idle.&lt;br&gt;
Rejected requests will not consume a token.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design decision:&lt;/strong&gt; The &lt;code&gt;AllowWithoutConsumingCapacity&lt;/code&gt; method is used. This method checks how much delay is required. If the delay is positive, the reservation is cancelled, and the request is rejected before any work is done, returning &lt;code&gt;429&lt;/code&gt; with a &lt;code&gt;Retry-After&lt;/code&gt; header in whole seconds. The rejected request keeps its token.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;reservation&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;limiter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Reserve&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;reservation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Delay&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="n"&gt;reservation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
 &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Rate limiting middleware uses a token bucket for each IP address. &lt;code&gt;429&lt;/code&gt; status code is returned with a delay in whole seconds.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No token is consumed on rejection. &lt;code&gt;X-Forwarded-For&lt;/code&gt; is trusted only when a trusted proxy is configured; with no proxy list, the socket address is used for identity.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4.3 The &lt;code&gt;raw_logs&lt;/code&gt; buffer
&lt;/h3&gt;

&lt;p&gt;I have a bounded channel that defaults to 10,000 entries for &lt;code&gt;RAW_LOGS_BUFFER&lt;/code&gt;. Validations happen quickly, but enrichments happen at their own pace.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design decision:&lt;/strong&gt; Producers wait for a bounded 100 ms rather than fail instantly. PushBatch rejects oversized batches immediately, then takes the producer lock and waits up to &lt;code&gt;CHANNEL_SEND_TIMEOUT&lt;/code&gt; 100 ms for room for the entire batch. This is blocking with a bounded timeout, providing backpressure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rawLogs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;cap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rawLogsQueue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rawLogsQueue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;capacityCheck&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;C&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
 &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;sendContext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ErrQueueOverloaded&lt;/span&gt;
 &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Writing straight to SQL would be on the slow path. Using the bounded channel means they are separated. I use back pressure by returning a &lt;code&gt;503&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.4 Enrichment
&lt;/h3&gt;

&lt;p&gt;Adds &lt;code&gt;received_at&lt;/code&gt;, &lt;code&gt;source_ip&lt;/code&gt;, and &lt;code&gt;environment&lt;/code&gt; (defaulting to production) from config. Also adds &lt;code&gt;log_id&lt;/code&gt;, &lt;code&gt;batch_id&lt;/code&gt;, and &lt;code&gt;retry_count&lt;/code&gt; (starting at 0) to every log. No I/O operation, so no failure case.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design decision:&lt;/strong&gt; All data is captured at the edge as it is based on request context. This means I only have access to the socket address, forwarded chain, and received time. These are stamped and then passed through. If any of these fields are missing, a sensible default is used.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EnrichedLog&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="n"&gt;LogEntry&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;rawLog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LogEntry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="n"&gt;LogID&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;       &lt;span class="n"&gt;logEnricher&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;logIDOrDefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rawLog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LogID&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
 &lt;span class="n"&gt;BatchID&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;rawLog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BatchID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="n"&gt;ReceivedAt&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="n"&gt;receivedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="n"&gt;SourceIP&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;sourceIP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="n"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;logEnricher&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;applicationEnvironment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="n"&gt;RetryCount&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;received_at&lt;/code&gt; uses the handler clock; &lt;code&gt;source_ip&lt;/code&gt; uses the socket address or the first valid &lt;code&gt;X-Forwarded-For&lt;/code&gt; header from behind a trusted proxy. All of these are captured at the edge.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.5 Routing
&lt;/h3&gt;

&lt;p&gt;The destination is stamped based on a validated mapping from the&amp;nbsp;&lt;code&gt;.env&lt;/code&gt; file using the &lt;code&gt;ROUTING_RULES=claims=service2,billing=service3&lt;/code&gt; format. If an unknown service is provided, it will default to &lt;code&gt;service1&lt;/code&gt;. The config is loaded using only &lt;code&gt;godotenv&lt;/code&gt; with no merging.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design decision:&lt;/strong&gt; This was a design decision to keep the routing rules in the&amp;nbsp;&lt;code&gt;.env&lt;/code&gt; file. I accept unknown services, as I can have new services logging and then add a new rule. All destination names are checked on startup and will fail if an unknown value is found. This means any typo will cause a crash on startup rather than the service being incorrectly routed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exists&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;serviceRouter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;enrichedLog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;exists&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="n"&gt;slog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"service has no routing rule, using default destination"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"service"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;enrichedLog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"default_destination"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;serviceRouter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;defaultDestination&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
 &lt;span class="n"&gt;enrichedLog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Destination&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;serviceRouter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;defaultDestination&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A validated map of &lt;code&gt;ROUTING_RULES&lt;/code&gt; has been created to route to either of the 3 destinations based on the service. The default destination has been set in the config and will show a warning if there is no rule to match.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.6 Queuing with RabbitMQ
&lt;/h3&gt;

&lt;p&gt;3 direct exchanges (&lt;code&gt;exchange_service1..3&lt;/code&gt;) are bound to durable queues with a routing key &lt;code&gt;log.write&lt;/code&gt;. Messages are published using &lt;code&gt;amqp.Persistent&lt;/code&gt;, with a 5-second timeout (&lt;code&gt;PUBLISH_TIMEOUT&lt;/code&gt;) and confirm mode. The publish will only return nil once the broker has acknowledged. The publisher will retry with backoff.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design decision:&lt;/strong&gt; Each destination uses an individual exchange and queue so that messages do not queue up in one queue, starving the others. We always get confirms and cannot allow fire-and-forget.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;publishError&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;publisher&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;publishOperation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;enrichedLog&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;publishError&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="n"&gt;publisher&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;markDisconnected&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
 &lt;span class="n"&gt;slog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"publishing to rabbitmq failed, using in-memory fallback"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;publishError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"destination"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;enrichedLog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Destination&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"batch_id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;enrichedLog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BatchID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;publisher&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;publishToFallbackAndRecord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;enrichedLog&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;publisher&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;recordPublishedMessage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The broker is a persistent buffer, so it can handle bursts of activity and restarts. If it goes offline, messages will be sent to an in-memory store with a maximum size. These messages will be written to the store and will never be republished. The publisher will retry with backoff.&lt;/p&gt;

&lt;p&gt;Timeouts on confirms do not necessarily mean the broker has failed. In ambiguous situations, the publisher will fall back to using the in-memory store. This means that a message that has been accepted by the broker may be stored twice. However, storage is idempotent based on the &lt;code&gt;log_id&lt;/code&gt;, so duplicate entries will be merged into a single row.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.7 Storage, retries and dead letters
&lt;/h3&gt;

&lt;p&gt;A single batch writer for SQLite. Consumes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;AMQP consumers&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fallback consumer&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;cmd/replay&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Flush per destination at 100 logs &lt;code&gt;WRITE_BATCH_SIZE&lt;/code&gt; or every 1 second &lt;code&gt;WRITE_FLUSH_INTERVAL&lt;/code&gt;, one transaction per destination. Failed flush retries: retry one after 1s, retry two after 5s, retry three after 10s. Four attempts over ~16s. If all fail, the whole batch is handed to a dead letter file as NDJSON, rotated by size.&lt;/p&gt;

&lt;p&gt;The dead letter file is appended to in NDJSON format. It will be rotated by closing and renaming the file. Each entry is written with a new line and flushed. If a crash occurs while writing, the last line may be truncated. &lt;code&gt;cmd/replay&lt;/code&gt; reads line by line and stops with an error on the first unparsable line, so a truncated tail causes replay to fail.&lt;/p&gt;

&lt;p&gt;As the same destination databases are opened by &lt;code&gt;cmd/replay&lt;/code&gt;, it is safe to run during the service. SQLite will serialise writes to the database per transaction. Both processes will use &lt;code&gt;WAL&lt;/code&gt; and a busy timeout. This means the replay process will wait for/obtain a write lock, and the file will never be corrupted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design decision:&lt;/strong&gt; Two triggers have been used to flush the data. This provides a good balance between latency and efficiency. Persistence is idempotent, as each &lt;code&gt;log_id&lt;/code&gt; will be persisted using &lt;code&gt;ON CONFLICT (log_id) DO NOTHING&lt;/code&gt; on the unique index. Duplicate rows will not occur, as messages will not be duplicated when redelivered, and dead letters will not be duplicated when replayed. The dead letter file can safely be replayed twice.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;deadLetterError&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;batchWriter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deadLetterBatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;enrichedLogs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;storeError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The writer will flush each destination once every 100 logs or 1s. If an error occurs, it will retry with a backoff. The &lt;code&gt;retry_count&lt;/code&gt; will be stamped, and the message will be dead-lettered. Recovery can be done using &lt;code&gt;cmd/replay&lt;/code&gt;, which will use the same idempotent &lt;code&gt;StoreBatch&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The dead letter store contains NDJSON format for each batch. It contains the full enriched log, storage exception, and time of failure.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.8 Observation
&lt;/h3&gt;

&lt;p&gt;The in-memory registry contains the following metrics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Receipts&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Request counts per level/service&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Rate-limited requests&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Dead letters&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Write errors&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;1-minute moving average of throughput&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Design decision:&lt;/strong&gt; Metrics are collected by the pipeline. The limiter collects the number of times it rejected a request. The writer collects dead letters and write errors. The queue backlog can come from either the RabbitMQ management API or estimated from publish minus store if the API isn't available.&lt;/p&gt;

&lt;p&gt;A single registry containing metrics from each stage. Backlog will be collected from local estimate if the management API isn't available. The metrics can be viewed as a JSON snapshot or via server-sent events.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Designing for failure
&lt;/h2&gt;

&lt;p&gt;The table maps scenarios to outcomes.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;The client sees&lt;/th&gt;
&lt;th&gt;Where the log ends up&lt;/th&gt;
&lt;th&gt;Section and path&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RabbitMQ unreachable&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;202&lt;/code&gt; still, if memory capacity lasts&lt;/td&gt;
&lt;td&gt;Stored directly by the fallback consumer, never re-published&lt;/td&gt;
&lt;td&gt;4.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RabbitMQ down and fallback full&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;202&lt;/code&gt; still&lt;/td&gt;
&lt;td&gt;Dead-letter NDJSON entry with the reason&lt;/td&gt;
&lt;td&gt;
4.6, publish error path to the sink&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQLite write failure&lt;/td&gt;
&lt;td&gt;Nothing, by design&lt;/td&gt;
&lt;td&gt;Retried at 1, 5, then 10 seconds, then the whole batch dead-lettered&lt;/td&gt;
&lt;td&gt;4.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Queue overloaded past the send timeout&lt;/td&gt;
&lt;td&gt;&lt;code&gt;503&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Durably spooled, not yet enqueued; will be replayed on restart, client may retry and create duplicates handled by idempotency&lt;/td&gt;
&lt;td&gt;4.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Body or batch oversized&lt;/td&gt;
&lt;td&gt;&lt;code&gt;413&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Not ingested, visibly rejected&lt;/td&gt;
&lt;td&gt;4.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wrong content type&lt;/td&gt;
&lt;td&gt;&lt;code&gt;415&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Not ingested, visibly rejected&lt;/td&gt;
&lt;td&gt;4.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-IP limit exceeded&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;429&lt;/code&gt; + &lt;code&gt;Retry-After&lt;/code&gt; in whole seconds&lt;/td&gt;
&lt;td&gt;Not ingested, visibly rejected, token kept&lt;/td&gt;
&lt;td&gt;4.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ungraceful crash&lt;/td&gt;
&lt;td&gt;Transport error&lt;/td&gt;
&lt;td&gt;In-memory buffer and writer batches lost; spooled batches survive on disk and are replayed on restart. What that costs is section 7
&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two failure mechanisms.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Closed/shutting down writer: Consumer nack's messages immediately, and the broker will redeliver shortly. This only happens if the writer is shutting down.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SQLite flush failure: Message will be scheduled up to 16s before dead-lettering. This is spread out to reduce retries.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  6. Proof
&lt;/h2&gt;

&lt;p&gt;I have 172 individual tests across 15 packages that are CI'd with the race detector. Timings are simulated using clock injection as I am testing out how the retry system deals with exhaustion in milliseconds.&lt;/p&gt;

&lt;p&gt;Integration tests under &lt;code&gt;cmd/server/integration_test.go&lt;/code&gt; use real components. This includes end-to-end functionality, zero dead letters during an outage, flush functionality triggered both ways, and retry to dead letter when the store fails.&lt;/p&gt;

&lt;p&gt;I also have a load that simulates a constant request rate with &lt;code&gt;cmd/loadtest&lt;/code&gt;. It uses fifty IPs in the documentation range as sources, and will output statistics on how many requests have gone through per status and latency percentiles.&lt;/p&gt;

&lt;p&gt;I ran this with &lt;code&gt;12&lt;/code&gt; pipeline workers, batch size &lt;code&gt;100&lt;/code&gt;, and &lt;code&gt;8&lt;/code&gt; senders. This gave me 300 requests, each with &lt;code&gt;100&lt;/code&gt; entries. This gave me &lt;code&gt;30,000&lt;/code&gt; logs accepted in total. I ran this on my laptop with RabbitMQ and SQLite, with spool fsync enabled.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Sustained target run&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sustained ingestion&lt;/td&gt;
&lt;td&gt;1000.0 logs/s, zero rejections&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Response latency&lt;/td&gt;
&lt;td&gt;mean 2.9 ms, p50 2.2 ms, p95 7.1 ms, p99 14.1 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;End-to-end accounting&lt;/td&gt;
&lt;td&gt;300 requests accepted, all 30,000 accepted logs stored when the queue drained 47 seconds later, no dead letters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server footprint&lt;/td&gt;
&lt;td&gt;~26 MB RSS, ~9 percent CPU&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This proves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Sustained acceptance rate of 1,000 logs/s on one laptop with local broker and SQLite, confirmed by row-count reconciliation.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;End-to-end with &lt;code&gt;30&lt;/code&gt; seconds of sending time and &lt;code&gt;47&lt;/code&gt; seconds of draining. Ingestion accepted at 1,000 logs/s while storage lagged; the backlog sat in the RabbitMQ queues. &lt;code&gt;30,000&lt;/code&gt; logs in &lt;code&gt;77&lt;/code&gt; seconds, about 390 logs/s including tail. This end-to-end rate reflects the publisher confirm loop, not consumers or SQLite. The test was mapped to a single service, so a single SQLite writer did the work; spreading across destinations does not change the per‑log confirm latency.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Does not prove:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Rate limiter saturation.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Multiple instances.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; When using 12 workers, you can get a throughput of 1,000 logs per second, but if you use the default of 4, you won't. You should aim for around 100 logs per second per worker. The confirmation loop is the limiting factor.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Trade-offs and what I would&amp;nbsp;change
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Log scale makes columnar storage ideal. Failure logic is contained within the code in SQLite. Batch writer means I can batch inserts. I would use ClickHouse in production. Two triggers mean the contract remains intact.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I guarantee at-least-once delivery by having an idempotent storage per &lt;code&gt;log_id&lt;/code&gt;. If a crash occurs between inserting a record and sending the ack message, then the records will be delivered again. The command and replay functionality means the dead letters are applied again. The log_id is set during ingestion, and using &lt;code&gt;ON CONFLICT DO NOTHING&lt;/code&gt; ensures only one row will exist for both.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Dead letters are applied at the batch level. After 16s of failed flushes, up to 99 logs that have done nothing wrong will be moved to a dead letter file. These are stored and can be restored. They are also grouped into batches.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In-memory state will be lost if there is an ungraceful crash. The writer has a buffer of less than 1s per destination, and the fallback will be lost. The broker will have the confirmed but unacknowledged messages. Spooled batches survive on disk and are replayed on restart, so admission is durable even if the process crashes; the cost is potential duplicates on retry.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The spool adds fsync latency on the request path and requires compaction of the spool log and &lt;code&gt;.acked&lt;/code&gt; files on restart. This is a deliberate trade-off for durable admission.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Having a single point of state makes scaling out difficult. I have a limiter, counters, and backlog estimation, which are held in memory. The second replica will have its own buckets; these will be reset when the service is restarted. Having a shared rate limiting store such as Redis and storing/persisting metrics allows scaling out.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No search endpoint. Initially, this wasn't searchable. Everything is stored centrally and monitored. Logs can be viewed through SQLite and dead letter tools. Next step is to implement full-text search on the logs table in SQLite.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No alerts. Counters are in place to trigger, but paging on a threshold with cooldown and dedupe is a monitoring-stack integration.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What I would improve:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Search endpoint&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;State for scaling out&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Alerts&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Performance tip: The max we can publish per log is the publish-confirmed rate. If I were to publish the entire batch per confirmation, then this would increase the max per worker.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Lessons
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Defence of the ingest boundary. Inexpensive inline decisions, and things to bound and make visible. All components have capacity and timeout so we can never block or drop.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Batching in async pipelines. We can be honest about our options. Either take the whole batch, or fail the whole request. The producer lock ensures we can do this under concurrency. When shutting down, we drain the channel, flush the writer, close the store, and lastly stop the dead letter writer.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Designing for queue/store failures. Each failure point has a different decision to make. If the broker fails, we use a one-time fallback store. If SQLite fails, we will retry with a limit, using stamps and dead-lettering out. If we fully fallback, we get dead letters. We can chain these decisions. It makes it easier to have a stage that can't fail.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Registry for event-to-action. The least appealing but most useful decision. We can pull from our registry. During integration testing, the broker failed, but the metrics endpoint still responded, as the fallback estimate was in the same registry. We can instrument what we are already doing. The signals will fail at the same time as the pipeline, meaning they are exactly when we need them.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Review Questions
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;One-line answer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Which fields are required, and which status codes can &lt;code&gt;POST /logs&lt;/code&gt; return?&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;timestamp&lt;/code&gt; (RFC 3339), &lt;code&gt;service&lt;/code&gt; (100 characters), &lt;code&gt;level&lt;/code&gt; (four values), &lt;code&gt;message&lt;/code&gt; (10,000 characters); &lt;code&gt;202&lt;/code&gt;, &lt;code&gt;400&lt;/code&gt;, &lt;code&gt;413&lt;/code&gt;, &lt;code&gt;415&lt;/code&gt;, &lt;code&gt;503&lt;/code&gt;, plus &lt;code&gt;429&lt;/code&gt; from the limiter.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;How does the rate limiter work, and what happens when the limit is exceeded?&lt;/td&gt;
&lt;td&gt;A per-IP token bucket in middleware before the handler; &lt;code&gt;429&lt;/code&gt; with &lt;code&gt;Retry-After&lt;/code&gt; in whole seconds, and the rejected request keeps its token.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Why an in-memory channel instead of writing straight to SQLite?&lt;/td&gt;
&lt;td&gt;The channel keeps the slow path out of the request and turns queue pressure into &lt;code&gt;503&lt;/code&gt; backpressure with a timed, all-or-nothing send.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Which enrichment fields are added, and where does each value come from?&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;received_at&lt;/code&gt; from the handler's clock, &lt;code&gt;source_ip&lt;/code&gt; from the socket address or a trusted proxy's &lt;code&gt;X-Forwarded-For&lt;/code&gt;, &lt;code&gt;environment&lt;/code&gt; from configuration, all captured at the edge.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;How are routing rules structured, and how is a database chosen?&lt;/td&gt;
&lt;td&gt;ROUTING_RULES from .env maps service to one of three destinations, stamped on the log, with the configured default and a warning when no rule matches.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Why RabbitMQ, and what changes when it is unavailable?&lt;/td&gt;
&lt;td&gt;The broker is the disk-backed buffer absorbing bursts; down means the bounded in-memory fallback stores directly, never re-publishes, and the publisher reconnects with backoff.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;How does batch writing work, and what is the retry strategy?&lt;/td&gt;
&lt;td&gt;One writer flushes per destination at 100 logs or one second inside a transaction, retries once per backoff delay stamping &lt;code&gt;retry_count&lt;/code&gt;, then dead-letters the batch.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;What reaches the dead-letter file, and how is it recovered?&lt;/td&gt;
&lt;td&gt;Batch-level entries as NDJSON with the log, the error and the timestamp, rotated by size, restored by &lt;code&gt;cmd/replay&lt;/code&gt; through the same idempotent &lt;code&gt;StoreBatch&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Which metrics are tracked, and what does &lt;code&gt;GET /metrics.json&lt;/code&gt; return?&lt;/td&gt;
&lt;td&gt;Receipt, level and service counts, error rate, throughput, queue backlog, rate-limited requests, dead letters and write failures, as one atomic JSON snapshot.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Which alert conditions exist, and how is alert spam prevented?&lt;/td&gt;
&lt;td&gt;None are built: the counters are ready to trigger against, and alerting with its cooldown design is a monitoring-stack integration named in section 7.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

</description>
      <category>go</category>
      <category>distributedsystems</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>A Story About Pluralization In Code (2 Items vs 2 Boxes)</title>
      <dc:creator>John Eliud Odhiambo</dc:creator>
      <pubDate>Mon, 05 Jan 2026 13:20:47 +0000</pubDate>
      <link>https://dev.to/johneliud/a-story-about-pluralization-in-code-2-items-vs-2-boxes-371g</link>
      <guid>https://dev.to/johneliud/a-story-about-pluralization-in-code-2-items-vs-2-boxes-371g</guid>
      <description>&lt;p&gt;I was building a small console application. A simple cart. It was nothing fancy but a chance to experiment with input, calculations, and formatted output. Here’s the first version I wrote:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import java.util.Scanner;

public class Cart {
    static void cart() {
        Scanner scanner = new Scanner(System.in);

        System.out.print("Enter item name: ");
        String item = scanner.nextLine();

        System.out.print("Enter price for each: ");
        double price = scanner.nextDouble();

        if (price &amp;lt;= 0) {
            throw new IllegalArgumentException("Price must be positive.");
        }

        System.out.print("Enter quantity to buy: ");
        int quantity = scanner.nextInt();

        if (quantity &amp;lt;= 0) {
            throw new IllegalArgumentException("Quantity must be positive.");
        }

        double total = price * quantity;

        String itemLabel = quantity == 1 ? item : item + "s";

        System.out.printf(
            "You bought %d %s. Your grand total is KES %.2f.%n",
            quantity, itemLabel, total
        );
    }

    static void main(String[] args) {
        cart();
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When I ran it with something simple like &lt;code&gt;apple&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Enter item name: apple
Enter price for each: 100
Enter quantity to buy: 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You bought 2 apples. Your grand total is KES 200.00.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Perfect, simple and predictable. It worked exactly as expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  The First Spark Of Curiosity
&lt;/h2&gt;

&lt;p&gt;But then I tried a word that wasn’t so straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Enter item name: box
Enter price for each: 100
Enter quantity to buy: 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the program printed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You bought 2 boxs. Your grand total is KES 200.00.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I paused and thought I could maybe just add some rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Words ending in &lt;code&gt;x&lt;/code&gt; get appended with &lt;code&gt;es&lt;/code&gt;. This would also apply for &lt;code&gt;s&lt;/code&gt;, &lt;code&gt;sh&lt;/code&gt;, and &lt;code&gt;ch&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Words ending with &lt;code&gt;y&lt;/code&gt; become &lt;code&gt;ies&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;But what about words like &lt;code&gt;mouse&lt;/code&gt; and &lt;code&gt;child&lt;/code&gt;?&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It was exciting as it felt like executing the real craft of programming beyond just making something work.&lt;/p&gt;

&lt;h2&gt;
  
  
  First Attempt Towards A Smarter Solution
&lt;/h2&gt;

&lt;p&gt;I decided to write a small helper function to handle the obvious cases:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;static String pluralize(String word, int quantity) {
    if (quantity == 1) return word;

    if (word.endsWith("s") || word.endsWith("x") || word.endsWith("z")
        || word.endsWith("ch") || word.endsWith("sh")) {
        return word + "es";
    }

    if (word.endsWith("y") &amp;amp;&amp;amp; word.length() &amp;gt; 1 
        &amp;amp;&amp;amp; !"aeiou".contains("" + word.charAt(word.length() - 2))) {
        return word.substring(0, word.length() - 1) + "ies";
    }

    return word + "s";
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, I could run tests with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;System.out.println(pluralize("box", 2));    // boxes
System.out.println(pluralize("city", 2));   // cities
System.out.println(pluralize("class", 2));  // classes
System.out.println(pluralize("apple", 2));  // apples
System.out.println(pluralize("mouse", 2));  // mouses
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The results were mostly correct; &lt;code&gt;boxes&lt;/code&gt;, &lt;code&gt;cities&lt;/code&gt;, &lt;code&gt;classes&lt;/code&gt; but &lt;code&gt;mouse&lt;/code&gt; to &lt;code&gt;mouses&lt;/code&gt; stood out. Irregular nouns weren’t going to be solved by simple rules. And every new edge case made the code longer and more fragile.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.reddit.com/r/ProgrammerHumor/comments/mddf8o/even_numbers_are_so_hard/#lightbox" rel="noopener noreferrer"&gt;This felt like the 'even numbers are hard' meme&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At this point, I realized that my 'make it perfect' curiosity was bumping into the limitations of language itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Discovering ICU4J
&lt;/h2&gt;

&lt;p&gt;Searching for solutions, I found ICU4J's Java library used in production for internationalization and pluralization and it looked promising. However after reading the documentation more carefully, I realized ICU4J doesn’t actually change the spelling of words. It decides when to use a singular versus plural form. For example, it can help you display:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You have 1 item
You have 2 items
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But it does not automatically turn &lt;code&gt;box&lt;/code&gt; into &lt;code&gt;boxes&lt;/code&gt; or &lt;code&gt;child&lt;/code&gt; into &lt;code&gt;children.&lt;/code&gt; It solves the category problem, not the word-inflection problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Tempting Idea
&lt;/h2&gt;

&lt;p&gt;It was disappointing after realizing that rule-based pluralization was incomplete and that even production-grade libraries like ICU4J weren’t meant to pluralize raw nouns.&lt;/p&gt;

&lt;p&gt;Just as I was about to give up, another thought crossed my mind:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;What if I just ask an AI to do this?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It felt reasonable as modern AI models are excellent at language. They know that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;box&lt;/code&gt; pluralizes to &lt;code&gt;boxes&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;class&lt;/code&gt; to &lt;code&gt;classes&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mouse&lt;/code&gt; to &lt;code&gt;mice&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;child&lt;/code&gt; to &lt;code&gt;children&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In theory, this meant making an API call, passing the noun, and getting back the correct plural. This guaranteed no grammar rules, and no irregular noun lists. Just letting the model handle the complexity.&lt;/p&gt;

&lt;p&gt;For a moment, this felt like the perfect solution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the AI Idea Actually Makes Sense (At First)
&lt;/h2&gt;

&lt;p&gt;To be fair, this approach isn’t foolish. In fact, it has real strengths because an AI model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Understands irregular nouns&lt;/li&gt;
&lt;li&gt;Handles borrowed words and edge cases&lt;/li&gt;
&lt;li&gt;Adapts to language naturally&lt;/li&gt;
&lt;li&gt;Requires very little code on my end&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a small personal project, the idea is genuinely attractive. I wouldn’t need to maintain pluralization logic at all. I’d just delegate the problem to the AI via an API.&lt;/p&gt;

&lt;p&gt;But then I paused and started thinking like a system designer, not just a coder.&lt;/p&gt;

&lt;h2&gt;
  
  
  The First Red Flag: Cost
&lt;/h2&gt;

&lt;p&gt;Pluralization is a low-value, high-frequency operation. If every time a user adds an item to a cart the app needs to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Make a network request&lt;/li&gt;
&lt;li&gt;Pay for tokens&lt;/li&gt;
&lt;li&gt;Wait for a response&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That cost adds up quickly. Paying for AI inference to decide whether to print &lt;code&gt;boxes&lt;/code&gt; or &lt;code&gt;items&lt;/code&gt; is hard to justify, especially when that same operation could be done locally with zero cost.&lt;/p&gt;

&lt;p&gt;For small scripts or experiments, this might be fine. At scale, it becomes expensive very fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Second Red Flag: Latency
&lt;/h2&gt;

&lt;p&gt;Pluralization sits on the critical path of user feedback. It happens:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;While rendering UI&lt;/li&gt;
&lt;li&gt;While printing output&lt;/li&gt;
&lt;li&gt;During fast interactions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An AI call introduces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Network latency&lt;/li&gt;
&lt;li&gt;Timeout risk&lt;/li&gt;
&lt;li&gt;Retry logic&lt;/li&gt;
&lt;li&gt;Failure modes you don’t control (500 errors)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Something as simple as printing a sentence suddenly depends on the availability of an external service. That outright felt wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Third Red Flag: Non-Determinism
&lt;/h2&gt;

&lt;p&gt;This was by far the biggest concern. Language is flexible, and AI reflects that. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;cactus&lt;/code&gt; to &lt;code&gt;cacti&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cactus&lt;/code&gt; to &lt;code&gt;cactuses&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both are correct but if the application prints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You bought 2 cacti
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and later:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You bought 2 cactuses
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You just introduced inconsistency. An answer that is 'sometimes right' is often worse than a simpler one that is always consistent.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fourth Red Flag: Control and Safety
&lt;/h2&gt;

&lt;p&gt;To pluralize a word using AI, I have to send user input to an external service. This raises the questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What if the item name contains sensitive information?&lt;/li&gt;
&lt;li&gt;What if this data must stay on-device?&lt;/li&gt;
&lt;li&gt;What happens if the API changes behavior?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Suddenly, a simple console program is tied to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Network access
-API keys&lt;/li&gt;
&lt;li&gt;Privacy considerations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At this point, the picture became clear. Opting to use AI for pluralization solves a hard linguistic problem but introduces cost, latency, inconsistency, dependency and still does not guarantee perfect results.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Practical Tradeoff
&lt;/h2&gt;

&lt;p&gt;At this point, I had to step back and ask myself what the real goal of my program is.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Showing the number of items bought&lt;/li&gt;
&lt;li&gt;Showing the total cost&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The exact spelling of the product name in plural wasn’t critical.&lt;/p&gt;

&lt;p&gt;That’s when it clicked almost every app I use does the same thing. Shopping sites, notification systems, and dashboards don’t attempt to pluralize arbitrary nouns. They simply use a safe, controlled noun:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You bought 2 items
You have 5 notifications
Your cart contains 3 products
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is predictable, safe, and scales well since there are no edge cases to handle and also works perfectly if you later translate your application into other languages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Code Snippet
&lt;/h2&gt;

&lt;p&gt;Here’s how my original shopping cart code evolved after all this reflection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;double total = price * quantity;

String message = quantity == 1 ? "item" : "items";

System.out.printf(
    "You bought %d %s. Your grand total is KES %.2f.%n",
    quantity, message, total
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You bought 1 item. Your grand total is KES 100.00.
You bought 2 items. Your grand total is KES 200.00.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No edge cases, no dictionary of irregular nouns, no fragile rules. This approach is exactly what large production systems do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Curiosity is important. Trying to improve the code teaches you a lot about programming, language, and tradeoffs.&lt;/li&gt;
&lt;li&gt;Language is messy and even simple pluralization can have dozens of edge cases.&lt;/li&gt;
&lt;li&gt;Production code values simplicity, correctness, and consistency.&lt;/li&gt;
&lt;li&gt;Using ICU4J helps in the right context and should be used for plural-aware messages and not for spelling every noun correctly.&lt;/li&gt;
&lt;li&gt;Opting for controlled vocabulary wins,is safe, predictable, and maintainable.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Thought
&lt;/h2&gt;

&lt;p&gt;The journey from &lt;code&gt;apple&lt;/code&gt; to &lt;code&gt;apples&lt;/code&gt; to understanding why &lt;code&gt;2 boxes&lt;/code&gt; is rarely shown in apps is more than a lesson in pluralization. It is a lesson in designing software systems that works reliably, even when human language does not cooperate.&lt;/p&gt;

&lt;p&gt;Sometimes, the simplest solution like displaying &lt;code&gt;2 items&lt;/code&gt; is also the most elegant.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>discuss</category>
    </item>
    <item>
      <title>The Hidden Environmental Cost of Your Cloud Storage</title>
      <dc:creator>John Eliud Odhiambo</dc:creator>
      <pubDate>Mon, 28 Jul 2025 17:54:27 +0000</pubDate>
      <link>https://dev.to/johneliud/the-hidden-environmental-cost-of-your-cloud-storage-58e1</link>
      <guid>https://dev.to/johneliud/the-hidden-environmental-cost-of-your-cloud-storage-58e1</guid>
      <description>&lt;p&gt;We live in the age of infinite storage. Photos, videos, and documents get uploaded to the cloud without a second thought. Google Drive, iCloud, and Dropbox all sell on the idea of limitless space. But here’s the thing: the cloud isn’t magic. It’s a physical network of massive, power-hungry data centers, and they’re leaving a bigger carbon footprint than you might realize.&lt;/p&gt;

&lt;p&gt;This made me write about the hidden environmental cost of cloud storage. Why "just uploading it" isn’t as clean as it seems, and what we can do about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  "The Cloud" Is Just Someone Else’s Computer (And It’s Using A Lot Of Energy)
&lt;/h2&gt;

&lt;p&gt;When you upload a file to the cloud, it doesn’t just float in space; it gets stored in a data center, a room full of servers running 24/7.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A single data center can use as much electricity as 50,000 homes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Globally, data centers account for 1-2% of the world’s electricity use, and that number is growing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Cooling these servers alone takes billions of gallons of water annually.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most of that energy isn’t even renewable. Many cloud providers still rely on fossil fuels to keep their data centers running. So every time you back up another 4K video of your pet, you’re indirectly contributing to that demand.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "Dark Data" Problem (Why Your Forgotten Files Matter)
&lt;/h2&gt;

&lt;p&gt;Think about how much stuff you’ve uploaded over the years. A lot of that just sits there, untouched. That’s called dark data. These are files stored but never used.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Estimates suggest up to 60% of stored data is dark data.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Keeping it all online means servers are working overtime to store useless files.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Deleting unnecessary files could cut energy waste significantly, but nobody does it.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s like leaving every light in your house on 24/7, even in rooms you never enter. Sure, one person doing it doesn’t matter, but multiply that by billions of users and suddenly it’s a big problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Can We Do? (Realistic Solutions)
&lt;/h2&gt;

&lt;p&gt;Cloud storage isn’t going away, but we can be smarter about it. Here’s how:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Clean Up Your Digital Hoarding&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Delete duplicates, blurry photos, and old files you’ll never need.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use tools like Google Photos' Free Up Space or iCloud’s storage manager.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Unsubscribe from services you don’t use.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;Choose Greener Cloud Providers&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Some companies are pushing for renewable energy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Google &amp;amp; Microsoft aim for carbon-neutral data centers.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Apple's iCloud runs on 100% renewable energy.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Smaller providers like GreenGeeks focus on sustainability.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;Local Backups Aren't Dead&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Instead of uploading everything, consider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;External SSDs for large files you rarely access.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Network-attached storage (NAS) for home backups.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Physical archives (hard drives still have a place).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final Thought: The Cloud Isn’t Evil, But It’s Not Invisible Either
&lt;/h2&gt;

&lt;p&gt;The convenience of cloud storage is undeniable, but like any tech, it comes with trade-offs. Small changes add up. If enough people clean up their unused files and push for greener tech, the cloud's environmental impact can shrink.&lt;/p&gt;

&lt;p&gt;So next time you get that storage full notification, maybe don't just blindly upgrade, but take a minute to delete what you don't need. The planet (and your wallet) might thank you.&lt;/p&gt;

</description>
      <category>cloud</category>
      <category>cloudstorage</category>
    </item>
    <item>
      <title>Understanding Load Shedding</title>
      <dc:creator>John Eliud Odhiambo</dc:creator>
      <pubDate>Tue, 24 Jun 2025 13:07:35 +0000</pubDate>
      <link>https://dev.to/johneliud/understanding-load-shedding-41md</link>
      <guid>https://dev.to/johneliud/understanding-load-shedding-41md</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In today’s digital landscape, applications must handle unpredictable traffic spikes without collapsing. Whether it’s a sudden surge in users or a distributed denial-of-service (DDoS) attack, systems need mechanisms to protect themselves from being overwhelmed. One such mechanism is load shedding, a defensive strategy that prioritizes critical functionality by selectively rejecting non-essential requests when the system is under stress.&lt;/p&gt;

&lt;p&gt;In this article, we’ll explore:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What is load shedding, and why is it necessary?&lt;/li&gt;
&lt;li&gt;A practical demo that shows load shedding in action.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By the end, you should understand how to apply this technique to keep your services resilient under pressure.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is Load Shedding?
&lt;/h2&gt;

&lt;p&gt;Load shedding is the deliberate termination or deferral of non-critical requests to prevent system overload. It ensures that essential services remain available even when demand exceeds capacity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Is It Necessary?
&lt;/h2&gt;

&lt;p&gt;Without load shedding, a system might experience:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Resource exhaustion: Too many requests consume CPU, memory, or network bandwidth.&lt;/li&gt;
&lt;li&gt;Cascading failures: One overloaded service can bring down dependent systems.&lt;/li&gt;
&lt;li&gt;Degraded performance: Even critical requests slow down, leading to timeouts and errors.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By shedding non-essential load, a system will:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Preserve core functionality (e.g., login, payment processing).&lt;/li&gt;
&lt;li&gt;Prevent total outages by avoiding system collapse.&lt;/li&gt;
&lt;li&gt;Improve user experience by failing fast instead of slow degradation.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How Load Shedding Works In Code
&lt;/h2&gt;

&lt;p&gt;Let us examine a practical implementation in Go and JavaScript. The demo consists of:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Essential services (always available).&lt;/li&gt;
&lt;li&gt;Non-essential services (shed under high load).&lt;/li&gt;
&lt;li&gt;A global request tracker to enforce limits.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Tracking Active Requests
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;shed_wrapper.go&lt;/strong&gt; file defines a middleware that monitors concurrent requests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;var (
    maxRequests     = int32(50)  // Maximum allowed concurrent requests
    currentRequests int32        // Global counter for active requests
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;maxRequests&lt;/strong&gt;: A threshold beyond which we start shedding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;currentRequests&lt;/strong&gt;: An atomic counter to track active requests safely.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Shedding Logic
&lt;/h3&gt;

&lt;p&gt;The ShedWrapper middleware wraps HTTP handlers and applies load shedding:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;func ShedWrapper(h http.HandlerFunc, canShed bool) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // Increment request count
        if atomic.AddInt32(&amp;amp;currentRequests, 1) &amp;gt; maxRequests {
            atomic.AddInt32(&amp;amp;currentRequests, -1) // Revert increment
            if canShed { // Only shed if allowed
                w.WriteHeader(http.StatusServiceUnavailable)
                fmt.Fprintln(w, "Service unavailable due to high load.")
                return
            }
        }
        defer atomic.AddInt32(&amp;amp;currentRequests, -1) // Decrement on exit
        h(w, r) // Proceed if under limit
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Key Decisions:
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;canShed flag: Determines whether a request can be rejected (e.g., non-essential vs. essential).&lt;/li&gt;
&lt;li&gt;Atomic operations: Ensure thread-safe increments/decrements.&lt;/li&gt;
&lt;li&gt;Graceful rejection: Returns HTTP 503 (Service Unavailable) for shed requests.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Essential vs. Non-Essential Services
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Essential Handler (essential_handler.go)&lt;/strong&gt;
Always processes requests, even under load:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fmt.Fprintf(w, "Essential service responded! (Load: %v)", simulatedLoad)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Non-Essential Handler (non_essential_handler.go)&lt;/strong&gt;
Rejects requests when load exceeds 50:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if simulatedLoad &amp;gt; 50 {
    w.WriteHeader(http.StatusServiceUnavailable)
    fmt.Fprintln(w, "Non-essential service unavailable!")
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Simulating Traffic Spikes
&lt;/h3&gt;

&lt;p&gt;The frontend &lt;strong&gt;(script.js)&lt;/strong&gt; lets users simulate load:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;async function simulateLoad(requestCount) {
    for (let i = 0; i &amp;lt; requestCount; i++) {
        fetch('/simulate-load'); // Triggers 2-second delay per request
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each &lt;strong&gt;/simulate-load&lt;/strong&gt; request &lt;strong&gt;(simulate_handler.go)&lt;/strong&gt; artificially increases load:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;func SimulateLoadHandler(w http.ResponseWriter, r *http.Request) {
    time.Sleep(2 * time.Second) // Simulate processing
    w.WriteHeader(http.StatusOK)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Putting It All Together
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Demo Workflow
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;User selects a load level (e.g., 75 concurrent requests).&lt;/li&gt;
&lt;li&gt;Essential services remain available even under high loads, demonstrating prioritization.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3p0c1e92e1m8kgz7f7w5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3p0c1e92e1m8kgz7f7w5.png" alt="Webpage screenshot demonstarting an essential service online even under high loads" width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The system tracks active requests:&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;If under 50, all services respond normally.&lt;/li&gt;
&lt;li&gt;If over 50, non-essential requests are rejected.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F63jvpcgfa3zuou0djhie.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F63jvpcgfa3zuou0djhie.png" alt="Webpage screenshot demonstarting a non-essential service online  under normal load" width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe0dt44s2rtdke7qxmlpf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe0dt44s2rtdke7qxmlpf.png" alt="Webpage screenshot demonstarting a non-essential service offline  under high loads to give priority to essential service" width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Try It Yourself
&lt;/h3&gt;

&lt;p&gt;You can experiment with this implementation by checking out the &lt;a href="https://github.com/johneliud/load_shedder" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt;. The demo includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A frontend to simulate load.&lt;/li&gt;
&lt;li&gt;Backend logic for essential/non-essential services.&lt;/li&gt;
&lt;li&gt;Configurable thresholds for testing.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Real-World Applications of Load Shedding
&lt;/h2&gt;

&lt;p&gt;While our demo is simplified, load shedding is used in many production systems:&lt;/p&gt;

&lt;h3&gt;
  
  
  E-Commerce Platforms
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Critical: Checkout, payment processing, inventory management.&lt;/li&gt;
&lt;li&gt;Non-Critical: Product recommendations, reviews, wishlist updates.&lt;/li&gt;
&lt;li&gt;During Black Friday sales, non-essential features might be temporarily disabled to ensure checkout remains stable.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Cloud APIs
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Critical: Authentication, billing, core compute services.&lt;/li&gt;
&lt;li&gt;Non-Critical: Logging, metrics, secondary APIs.&lt;/li&gt;
&lt;li&gt;If a cloud provider faces unexpected demand, it may throttle non-critical API calls.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Social Media Platforms
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Critical: Posting content, direct messaging.&lt;/li&gt;
&lt;li&gt;Non-Critical: "Like" counts, friend suggestions.&lt;/li&gt;
&lt;li&gt;During viral events, platforms might delay updating engagement metrics to prioritize content delivery.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Financial Systems
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Critical: Stock trading, fund transfers.&lt;/li&gt;
&lt;li&gt;Non-Critical: Transaction history, analytics.&lt;/li&gt;
&lt;li&gt;At market open, trading systems prioritize order execution over historical data queries.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Advanced Considerations
&lt;/h2&gt;

&lt;p&gt;While our demo uses a fixed threshold, real-world systems might:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Adjust maxRequests dynamically based on CPU/memory usage.&lt;/li&gt;
&lt;li&gt;Implement retries for shed requests with exponential backoff.&lt;/li&gt;
&lt;li&gt;Use circuit breakers to fail fast when dependencies are struggling.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Load shedding is a proactive way to handle traffic surges. By:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Monitoring request volume,&lt;/li&gt;
&lt;li&gt;Prioritizing critical functions, and&lt;/li&gt;
&lt;li&gt;Gracefully degrading non-essential features,&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;we can build systems that fail well instead of collapsing entirely. Try the &lt;a href="https://load-shedder.onrender.com" rel="noopener noreferrer"&gt;demo&lt;/a&gt; yourself to see load shedding in action!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>security</category>
      <category>development</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Building a Dynamic User Profile Dashboard with GraphQL</title>
      <dc:creator>John Eliud Odhiambo</dc:creator>
      <pubDate>Sat, 24 May 2025 18:50:38 +0000</pubDate>
      <link>https://dev.to/johneliud/building-a-dynamic-user-profile-dashboard-with-graphql-670</link>
      <guid>https://dev.to/johneliud/building-a-dynamic-user-profile-dashboard-with-graphql-670</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In today's web development landscape, efficient data fetching is crucial for creating responsive and user-friendly applications. This article explores a project that leverages GraphQL to create a personalized profile dashboard for Zone01 users, demonstrating how modern web technologies can be combined to deliver a seamless user experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Overview
&lt;/h2&gt;

&lt;p&gt;The GraphQL Profile Dashboard is a web application that connects to Zone01's GraphQL API to fetch and display user data in an interactive and visually appealing manner. The project showcases several key features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Secure JWT Authentication&lt;/strong&gt;: Users can log in with their Zone01 credentials to access their personalized dashboard&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic Data Visualization&lt;/strong&gt;: Interactive SVG-based charts display user progress and statistics&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Responsive Design&lt;/strong&gt;: A clean, modern interface that adapts to different screen sizes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dark/Light Theme Toggle&lt;/strong&gt;: User preference-based theming with persistent settings&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Technical Implementation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Authentication Flow
&lt;/h3&gt;

&lt;p&gt;The application implements a secure authentication flow using JWT (JSON Web Tokens). When users provide their credentials, the application makes a request to Zone01's authentication endpoint, receives a JWT token, and stores it in the browser's localStorage for subsequent API requests.&lt;/p&gt;

&lt;p&gt;This approach provides a secure way to maintain user sessions without storing sensitive credentials on the client side. The authentication status is monitored throughout the application, ensuring that protected routes are only accessible to authenticated users.&lt;/p&gt;

&lt;h3&gt;
  
  
  GraphQL Integration
&lt;/h3&gt;

&lt;p&gt;At the core of this application is its integration with Zone01's GraphQL API. Unlike traditional REST APIs, GraphQL allows the client to specify exactly what data it needs, reducing over-fetching and under-fetching of data.&lt;/p&gt;

&lt;p&gt;The application uses a custom GraphQL client that handles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Authentication header inclusion&lt;/li&gt;
&lt;li&gt;Error handling and token expiration&lt;/li&gt;
&lt;li&gt;Structured query execution&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's an example of how the application queries user data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;USER_PROFILE_QUERY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`query{
  user{
    id
    firstName
    lastName
    login
    email
    campus
    auditRatio
    totalUp
    totalDown
    totalXp: transactions_aggregate(where:{type:{_eq:"xp"},eventId:{_eq:75}}){
      aggregate{
        sum{
          amount
        }
      }
    }
    // Additional fields...
  }
}`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This query fetches all the necessary user information in a single request, which is then processed and displayed across different sections of the dashboard.&lt;/p&gt;

&lt;h3&gt;
  
  
  Data Visualization
&lt;/h3&gt;

&lt;p&gt;One of the standout features of this application is its use of custom SVG-based data visualizations. Rather than relying on heavy third-party libraries, the project implements lightweight, custom visualization components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Line Graphs&lt;/strong&gt;: For tracking XP progress over time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bar Graphs&lt;/strong&gt;: For comparing project completion statistics&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pie Charts&lt;/strong&gt;: For displaying audit ratios&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Donut Charts&lt;/strong&gt;: For showing pass/fail ratios&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These visualizations are created using SVG elements, providing a responsive and interactive experience while maintaining a small footprint.&lt;/p&gt;

&lt;h3&gt;
  
  
  Theme Management
&lt;/h3&gt;

&lt;p&gt;The application implements a theme management system that allows users to toggle between light and dark modes. This preference is stored in localStorage, ensuring that the user's preferred theme persists across sessions.&lt;/p&gt;

&lt;p&gt;The theme implementation uses CSS variables and class-based styling, making it easy to maintain and extend:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--transition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;all&lt;/span&gt; &lt;span class="m"&gt;0.5s&lt;/span&gt; &lt;span class="n"&gt;ease&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--text-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--dark-text-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#fff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--primary-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;rgb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;62&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;62&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--light-bg-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#fff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--dark-bg-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#121212&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--box-shadow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.35&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="m"&gt;0px&lt;/span&gt; &lt;span class="m"&gt;5px&lt;/span&gt; &lt;span class="m"&gt;15px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--dark-box-shadow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="m"&gt;0px&lt;/span&gt; &lt;span class="m"&gt;5px&lt;/span&gt; &lt;span class="m"&gt;15px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  User Experience
&lt;/h2&gt;

&lt;p&gt;The dashboard is designed with user experience at its core. Upon logging in, users are presented with a clean, organized interface that provides immediate access to their most important information:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Basic Information&lt;/strong&gt;: User details including name, email, and campus&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit Statistics&lt;/strong&gt;: Visualization of audit ratios and activity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;XP Statistics&lt;/strong&gt;: Progress tracking with interactive charts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Project Statistics&lt;/strong&gt;: Overview of completed and current projects&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skills&lt;/strong&gt;: Visualization of skill development across different areas&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Navigation is streamlined through a sidebar menu that allows users to quickly jump to different sections of their profile.&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenges and Solutions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Authentication Security
&lt;/h3&gt;

&lt;p&gt;Implementing secure authentication required careful consideration of token storage and transmission. The application uses localStorage for token storage, which provides a balance between persistence and security. All API requests include the JWT in the Authorization header, ensuring secure communication with the backend.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dynamic Data Loading
&lt;/h3&gt;

&lt;p&gt;Fetching and processing complex nested data from GraphQL required a structured approach to data handling. The application implements a loading state system that provides visual feedback during data fetching operations, enhancing the perceived performance of the application.&lt;/p&gt;

&lt;h3&gt;
  
  
  SVG Rendering Compatibility
&lt;/h3&gt;

&lt;p&gt;Creating cross-browser compatible SVG visualizations presented challenges, particularly with text rendering and animation. The solution involved implementing fallback options and ensuring that visualizations degrade gracefully in environments with limited support.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;This GraphQL Profile Dashboard demonstrates how modern web technologies can be combined to create a powerful, user-friendly application. By leveraging GraphQL for efficient data fetching, implementing custom SVG visualizations, and focusing on user experience, the project delivers a polished and performant solution for displaying user profile data.&lt;/p&gt;

&lt;p&gt;The modular architecture and clean separation of concerns make the codebase maintainable and extensible, providing a solid foundation for future enhancements. Whether you're looking to implement GraphQL in your own projects or seeking inspiration for data visualization techniques, this project offers valuable insights and practical examples.&lt;/p&gt;

&lt;h2&gt;
  
  
  Live Demo
&lt;/h2&gt;

&lt;p&gt;Experience the GraphQL Profile Dashboard in action at &lt;a href="https://graphql-uvsx.onrender.com" rel="noopener noreferrer"&gt;https://graphql-uvsx.onrender.com&lt;/a&gt;. Note that you'll need Zone01 credentials to log in and explore the full functionality.&lt;/p&gt;

</description>
      <category>graphql</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Will A Project Manager Expect Nine Expectant Mothers To Deliver Nine Babies In One Month? A Dive Into Project Management</title>
      <dc:creator>John Eliud Odhiambo</dc:creator>
      <pubDate>Sat, 10 May 2025 19:30:03 +0000</pubDate>
      <link>https://dev.to/johneliud/will-a-project-manager-expect-nine-expectant-mothers-to-deliver-nine-babies-in-one-month-a-dive-1dp1</link>
      <guid>https://dev.to/johneliud/will-a-project-manager-expect-nine-expectant-mothers-to-deliver-nine-babies-in-one-month-a-dive-1dp1</guid>
      <description>&lt;p&gt;There has always been a joke in project management that tries to paint a picture of how project managers plan and manage projects, as well as people. This has always been about project managers (PMs) expecting nine expectant mothers to deliver nine babies in one month. The purpose of this article will be to give my opinion on how truthful or false this statement is, what role PMs play in any project, my experience as a technical project manager during my internship, and my advice to any newbie PM.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; The article will be more specific about technical project management based on my software development background, but most opinions will be applicable to the broader project management field.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What Is Project Management?
&lt;/h2&gt;

&lt;p&gt;In simple terms, it's all about the process of planning, organizing, and overseeing to achieve a specific goal within defined constraints, scope, time, and budget.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who Is A PM, And What Role Do They Play?
&lt;/h2&gt;

&lt;p&gt;I define a PM as someone responsible for creating a clear vision for the project, building trust with all stakeholders, and ensuring everyone understands their role in the bigger picture, as well as identifying and managing risks.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is Agile Methodology?
&lt;/h2&gt;

&lt;p&gt;This is one of the approaches used in project management that emphasizes flexibility and adaptability. Work is broken into short cycles, often referred to as sprints, that last for a few weeks. This allows teams to deliver parts of the project incrementally, gather feedback quickly, and make adjustments as needed throughout the process.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Project I Am Working On
&lt;/h2&gt;

&lt;p&gt;In the spirit of bringing like-minded people together, my team has been tasked with developing a web platform that will provide a medium for enabling creatives and activists to identify and network with fellow creatives, discover places of interest, and collaborate on projects. I won't share many details for now due to organizational regulations.&lt;/p&gt;

&lt;p&gt;It takes great people to come up with a great product, and this is reflected in this project as we have a team comprising two designers, four developers, two PMs, the design team lead, the development team lead, and the PM team lead. The size of the team already justifies how high the stakes are for the project.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Sprint
&lt;/h2&gt;

&lt;p&gt;With agile as our approach for the project delivery, the first objective was to align with all stakeholders, including the client, for familiarity. This is crucial to clearly define everyone's role and foster the spirit of collaboration. To effectively manage the team, we leveraged project management tools like Jira, Confluence, GitHub Projects, as well as meeting tools like Google Meet. Jira and GitHub Projects are used to assign tasks, supervise individual progress on the assigned tasks, alongside estimate the schedule for each assigned task. It is from this that a PM can determine if everything is progressing as scheduled. An alternative to Jira can be Asana. To better manage our design team workflows, we opted for Jira and GitHub Projects for the development team.&lt;/p&gt;

&lt;p&gt;Apart from issuing tasks, a lot of documentation on every event that takes place, be it client meetings, team stand-ups, and so on, needs to be captured. For this, we used both Confluence for managing team events such as daily stand-ups, retrospectives, and meeting notes, and Google Docs for sharing project essentials with the client. For every meeting that happens, a good PM should always ensure they have a notebook and a pen. In the age of artificial intelligence, some note-taking assistants can also do this, but are often limited to some selected languages and dialects.&lt;/p&gt;

&lt;p&gt;It is also at the sprint phase where we establish our work processes that include daily stand-ups. A stand-up is a very brief meeting among the internal team, usually about fifteen minutes, conducted as the first thing in the morning, that seeks to set the mood for every team member for the day. It highlights the problems/blockers faced by an individual and their priority for the day. During this meeting, a PM needs to act fast to address any blockers that are delaying an individual's progress.&lt;/p&gt;

&lt;p&gt;For the project the team was working on, the first sprint deliverable was for our design team to have high fidelity prototypes for both web and mobile screens of the landing page, signup, and login. The second sprint was for our development team to implement the design templates into actual code for both the frontend and backend.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Highs: Moments That Made Me Believe
&lt;/h2&gt;

&lt;p&gt;The toughest task our team has is to ensure that what we deliver is in line with the client's vision, as well as manage client expectations. Among the presentations we have had with the client was a review of what was achieved during sprint one. I was very nervous during the entire presentation since any feedback from the client, whether positive or negative, at this point would have a change in the trajectory of the project's overall direction. Apart from some minor adjustments to the designs suggested by the client, the meeting ended on a high note as the presented deliverables were approved. The same also applied to the second sprint deliverable meeting. Such moments where the client expresses their gratitude are what every PM should strive for. This will involve frequent meetings with the client and asking a lot of questions to ensure you share the same vision as the client. I believe a PM should have a vast understanding of the project, as compared to the client, because you get to interact with every stakeholder in the project. The team spirit and morale are always boosted on such occasions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Lows: Moments That Made Me Doubt
&lt;/h2&gt;

&lt;p&gt;Despite the bed of roses, there is always a period of uncertainty and pressure. One such moment was during the second sprint when we faced version control merge conflicts while integrating the backend and frontend, with barely thirty minutes to a presentation meeting. As a PM, I initially assumed (the worst mistake a PM can make) that the handoff would be seamless after all, we had stand-ups, documentation, and alignment meetings. But I quickly learned that even with the best planning, miscommunications can occur, especially when team members interpret requirements differently. To mitigate this, we have embraced continuous communication, especially among our internal team, to ensure we all know what each person is doing.&lt;/p&gt;

&lt;p&gt;Time estimation is also a tricky part. Early in the project, we underestimated the time required for deliverables, which caused a delay in one of our internal milestones. We had to regroup, reassess, and communicate this shift in expectations to the client with transparency. This was tough but taught me the value of managing not only tasks but also expectations, both for the team and stakeholders.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons I Have Learned So Far
&lt;/h2&gt;

&lt;p&gt;One of the biggest lessons I have learned is that communication is important to any project. It is not about overcommunicating, but about ensuring that everyone knows the WHY behind what they are doing. A team that understands the end goal is more motivated and aligned.&lt;/p&gt;

&lt;p&gt;Another important realization is that leadership as a PM isn’t about having all the answers. It is about being resourceful enough to ask the right questions and build bridges when gaps appear.&lt;/p&gt;

&lt;p&gt;It is also crucial to adapt. What worked in the first sprint might not work for the next sprint. Flexibility in processes and even timelines can make the difference between a bottleneck and a breakthrough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do I Expect Nine Expectant Mothers to Deliver Nine Babies in One Month?
&lt;/h2&gt;

&lt;p&gt;Let me finally address the elephant in the room. A good PM needs to understand that just because you have more hands doesn’t mean the work gets done faster and to the required standards. Sometimes, it’s about the right skills, the right timing, and respecting the natural rhythm of work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Advice To New PMs
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Be curious by learning the basics of the tools your team is using. It helps in making informed decisions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Apart from chasing deadlines, build relationships by getting to know your team, understanding their strengths, and supporting them as people. Have open sessions with your team to get a better perspective of them.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Don’t be afraid to ask whether it's clarity from a client or technical help from your dev lead.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Always document decisions, changes, and feedback. This will come in handy at some points of the project cycle.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Celebrate small wins as they build momentum and remind everyone of the progress being made.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Conduct A/B tests among a number of users on occasions where a decision has to be made from multiple options. This builds trust, especially from the client.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As we progress more into the project, I will document the experience and publish another article based on a continuation of this. I hope this was worth reading. Thanks.&lt;/p&gt;

</description>
      <category>management</category>
      <category>projectmanagement</category>
    </item>
    <item>
      <title>FETCHING ADVENT OF CODE INPUT DYNAMICALLY IN GO</title>
      <dc:creator>John Eliud Odhiambo</dc:creator>
      <pubDate>Wed, 04 Dec 2024 20:04:23 +0000</pubDate>
      <link>https://dev.to/johneliud/fetching-advent-of-code-inputs-dynamically-in-go-1c67</link>
      <guid>https://dev.to/johneliud/fetching-advent-of-code-inputs-dynamically-in-go-1c67</guid>
      <description>&lt;p&gt;Advent of Code is a fun way for programmers to test and improve their problem-solving skills. While solving the puzzles, you might want to automate the fetching of your personalized puzzle input directly using its URL instead of copying the input to a text file that will be available locally. However, trying to access the input URL using a simple HTTP request, results in the message below:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Puzzle inputs differ by user. Please log in to get your puzzle input.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This article explains why this happens and how to correctly fetch your inputs dynamically using Go programming language.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Problem: Why Can't We Fetch The Input Directly?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Advent of Code requires you to log in to access your personalized puzzle inputs. When you log in through the browser, Advent of Code sets a session cookie in your browser. This cookie is used to identify your account and provide your unique input.&lt;/p&gt;

&lt;p&gt;If your HTTP requests don’t include this session cookie, the Advent of Code server cannot recognize you as a logged-in user, hence the error message.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution: Using the Session Cookie in HTTP Requests&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We must include the session cookie in our HTTP requests to fetch the puzzle input. Here is a step-by-step guideline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Log in to Advent of Code.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Open your browser's Developer Tools (Press F12 key) and navigate to the Network tab.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Refresh the Advent of Code page and look for the cookie header in the request headers.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm1hyayiktm92axd7og4u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm1hyayiktm92axd7og4u.png" alt="Screenshot showing developers tools" width="800" height="478"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Extract the value of the session cookie.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn8brceblo16gyn3b7ne9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn8brceblo16gyn3b7ne9.png" alt="Screenshot showing developers tools session cookie" width="800" height="478"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;NOTE: It's important to keep your session cookie a secret since someone else can access your Advent of Code account if they get access to it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Code To Fetch The Input&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Below is a simple program we will use to fetch our puzzle input dynamically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Setting Up The Base URL&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We start by defining the base URL for fetching inputs and creating a function to read the input for a specific day.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const baseURL = "https://adventofcode.com/2024/day/%s/input"

func readInput(day string) {
    url := fmt.Sprintf(baseURL, day)
    fmt.Println(url)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Creating The HTTP Request&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next, we create an HTTP request and include the session cookie.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;client := &amp;amp;http.Client{}
    req, err := http.NewRequest("GET", url, nil)
    if err != nil {
        fmt.Printf("Error creating HTTP request: %v\n", err)
        return
    }

    // Add the session cookie
    req.Header.Add("Cookie", "session=[YOUR_SESSION_TOKEN]")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;http.NewRequest&lt;/code&gt;: Creates an HTTP GET request for the input URL.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;req.Header.Add&lt;/code&gt;: Adds a header to the request with the session token for authentication. (Replace [YOUR_SESSION_TOKEN] with your actual token).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sending The Request And Handling The Response&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now we send the HTTP request and read the server's response.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;resp, err := client.Do(req)
    if err != nil {
        fmt.Printf("Error making HTTP request: %v\n", err)
        return
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        fmt.Printf("Unexpected HTTP status: %d\n", resp.StatusCode)
        return
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;client.Do(req)&lt;/code&gt;: Sends the HTTP request and stores the response.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;defer resp.Body.Close()&lt;/code&gt;: Ensures the response body is closed after reading.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;resp.StatusCode&lt;/code&gt;: Checks the HTTP status code. A code other than 200 indicates an error.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reading And Printing The Input&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Finally, we read the response body and print the puzzle input.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;body, err := io.ReadAll(resp.Body)
    if err != nil {
        fmt.Printf("Error reading response body: %v\n", err)
        return
    }

    fmt.Println(string(body))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;io.ReadAll(resp.Body)&lt;/code&gt;: Reads the response body.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;string(body)&lt;/code&gt;: Converts the body from a slice of bytes to a string for easy display.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Defining The Main Function&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We invoke the readInput function from the main function to fetch the input for day 1.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;func main() {
    readInput("1") // Fetches input puzzle for day 1
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Enhancing Security&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Hardcoding the session token in our code isn’t safe. Instead, we should store it as an environment variable using the steps below:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Export the session token using the terminal:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export AOC_SESSION="[YOUR_SESSION_TOKEN]"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Modify the code to read the session token from the environment variable. (Ensure to have "os" among your imports):
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;req.Header.Add("Cookie", "session="+os.Getenv("AOC_SESSION"))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This helps, the session token stay outside the source code, reducing the risk of accidental exposure.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Full Program Code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's the complete program for reference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;package main

import (
    "fmt"
    "io"
    "net/http"
    "os"
)

const baseURL = "https://adventofcode.com/2024/day/%s/input"

func readInput(day string) {
    url := fmt.Sprintf(baseURL, day)

    client := &amp;amp;http.Client{}
    req, err := http.NewRequest("GET", url, nil)
    if err != nil {
        fmt.Printf("Error creating HTTP request: %v\n", err)
        return
    }

    // Use session token from environment variable
    req.Header.Add("Cookie", "session="+os.Getenv("AOC_SESSION"))

    resp, err := client.Do(req)
    if err != nil {
        fmt.Printf("Error making HTTP request: %v\n", err)
        return
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        fmt.Printf("Unexpected HTTP status: %d\n", resp.StatusCode)
        return
    }

    body, err := io.ReadAll(resp.Body)
    if err != nil {
        fmt.Printf("Error reading response body: %v\n", err)
        return
    }

    fmt.Println(string(body))
}

func main() {
    readInput("1")
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Output&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu3mps71qs7a9asbep9xv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu3mps71qs7a9asbep9xv.png" alt="Screenshot showing output" width="800" height="478"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Things To Keep In Mind&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Session Expiry: Session tokens may expire after a while. If you encounter issues, log in again and retrieve a fresh token.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Privacy: Never share your session token publicly, including in blog posts or GitHub repositories.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You can dynamically fetch your Advent of Code inputs by including your session cookie in HTTP requests.&lt;/p&gt;

&lt;p&gt;Feel free to share your tips or ask questions in the comment section. Happy coding, and good luck with Advent of Code 2024!&lt;/p&gt;

</description>
      <category>go</category>
      <category>adventofcode</category>
      <category>coding</category>
    </item>
    <item>
      <title>MY EXPERIENCE ATTENDING GOPHERCON AFRICA 2024</title>
      <dc:creator>John Eliud Odhiambo</dc:creator>
      <pubDate>Tue, 12 Nov 2024 01:18:04 +0000</pubDate>
      <link>https://dev.to/johneliud/my-experience-attending-gophercon-africa-2024-171e</link>
      <guid>https://dev.to/johneliud/my-experience-attending-gophercon-africa-2024-171e</guid>
      <description>&lt;h2&gt;
  
  
  What is Gophercon?
&lt;/h2&gt;

&lt;p&gt;GopherCon is a global series of conferences dedicated to the Go programming language that provides a space for learning, networking, and collaboration within the Go community.&lt;/p&gt;

&lt;p&gt;As the first-ever GopherCon Africa ended after a two-day successful conference, I reflected on what a monumental event it was for the Go programming community on the continent. Despite being the first of its kind, GopherCon Africa 2024 exceeded all expectations by bringing together developers of diverse experience levels, offering state-of-the-art talks, and creating countless opportunities for networking and growth for anyone who attended.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Promising Beginning for the African Go Community
&lt;/h2&gt;

&lt;p&gt;This being the inaugural GopherCon in Africa made it special. It felt like the start of something big and something that would grow and shape the future of the Go language across the continent. Attendees, speakers, and organizers all shared a sense of pride in being part of something so significant.&lt;/p&gt;

&lt;p&gt;What made the event particularly successful was the way it struck a balance between serving seasoned developers and those just starting with Go. I earlier feared that the talks might cater only to advanced developers, leaving beginners behind, but the speakers did an exceptional job of making their content accessible to all, especially by giving room for asking as many questions as possible. Whether being interested in the technical talks or wanting to understand the broader vision for Go's future, there was something for everyone.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Venue Fit for Innovation
&lt;/h2&gt;

&lt;p&gt;The venue itself was nothing short of world-class. It was not just a place to sit and listen but a space that fostered meaningful conversations, learning, and connection. The environment played a huge role in the success of the event with modern facilities and thoughtful setups for both the talks and networking sessions. The venue elevated the experience and allowed me to focus on the content and connections.&lt;/p&gt;

&lt;h2&gt;
  
  
  State-of-the-Art Talks
&lt;/h2&gt;

&lt;p&gt;The talks were the heart of the event, and they certainly delivered. I had the privilege of hearing from industry leaders who presented on a wide range of topics, from event-driven architecture to advanced testing strategies. One talk that stood out to me was a session on "Empowering the Future with Go: Africa’s Role in the Go Community." It was inspiring to hear how Africa can lead the charge in growing Go adoption globally. Adesina Hassan’s quote "If there’s no community around you, build one" hit home for me. It was a reminder that the future of Go in Africa is in our hands, and we all have a part to play. &lt;br&gt;
The technical depth of the sessions was complemented by the speakers' ability to make the content relatable and actionable. Below are some topics that were covered at the conference: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Empowering the Future with Go: Africa’s Role in the Go Community&lt;br&gt;
The keynote underscored the importance of building communities and encouraged developers to initiate Go user groups locally. The speakers’ message emphasized Africa’s unique role in advancing Go, highlighting how community-driven support can make a lasting impact.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Event-Driven Architecture with Go and Kafka &lt;br&gt;
This session explored how Go can be used to create robust, event-driven architectures with Kafka. The talk covered the benefits of decoupling services, which improves scalability and resilience, and demonstrated how to use Kafka’s features to build a seamless data flow in distributed systems.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Effective Testing Strategies for Large Data Inserts &lt;br&gt;
We learned strategies to ensure efficient, resilient, and scalable data operations. From unit and integration testing to load testing and benchmarking, the session emphasized the need for data integrity checks, latency testing, and optimizing indexing strategies for handling large data volumes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Advanced Monitoring and Observability with OpenTelemetry and APItoolkit&lt;br&gt;
This hands-on workshop provided a deep dive into leveraging OpenTelemetry alongside APItoolkit to develop comprehensive monitoring and observability for modern applications. Attendees learned practical techniques to instrument distributed systems and analyze telemetry data for improved insights.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SDK Secrets: Unlocking API Magic &lt;br&gt;
This session was a deep dive into building efficient and user-friendly Software Development Kits (SDKs) for APIs. Key focus areas included error handling, concurrency, and designing SDKs that meet real user needs with structured logging, testing, and a strong emphasis on developer empathy.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Unveiling the Power of Interfaces: Go’s Secret Weapon &lt;br&gt;
Go’s approach to interfaces stands out due to its implicit implementation and structural typing. This talk not only highlighted the benefits of Go’s interfaces for achieving modular and testable code but also delved into advanced techniques, comparing Go’s interface capabilities with those of other languages.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Taming Code Chaos: Reducing Entropy in Go Design Systems &lt;br&gt;
Addressing common sources of code entropy, such as unstructured code and inconsistent styles, this session focused on modular architecture, dependency injection, and interface-driven design to create maintainable and scalable systems. The practical refactoring techniques discussed were valuable for increasing team efficiency.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Mastering Intra-service Communication in Go with gRPC &lt;br&gt;
This technical session examined how Go and gRPC simplify communication in microservices architectures, addressing issues like latency, performance, and reliability. Comparing gRPC with REST, the talk demonstrated how gRPC can streamline cross-service interactions, particularly for high-performance applications.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Testing in Go: Unlocking Confidence and Efficiency &lt;br&gt;
This talk emphasized the importance of test-driven development and table-driven tests, explaining how testing improves focus and builds confidence for developers. I learned practical techniques, including subtests, parallelism in testing, and using popular test packages like Testify and GoConvey to create robust test suites.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The State of Go in Africa &lt;br&gt;
In a special panel discussion, industry leaders discussed the unique challenges and growth opportunities for Go across the continent. Panelists shared insights on Go adoption and how Go is being introduced in universities and boot camps across the region.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of these sessions provided actionable insights and real-world examples, reinforcing why Go continues to grow in popularity for both small and large-scale applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Importance of Communities and Open-Source Contributions
&lt;/h2&gt;

&lt;p&gt;A key theme throughout the event was the importance of communities. I repeatedly heard that joining (or starting) a Go community is one of the best ways to grow as a developer. Being part of a community helps one learn faster, share knowledge, and stay up-to-date with the latest developments in the Go ecosystem. And it’s not just about personal growth; communities help raise awareness about Go, making it more accessible to others and driving adoption.&lt;/p&gt;

&lt;p&gt;Open-source contributions were also highlighted as an avenue to not only improve our technical skills but also gain visibility in the wider tech world. Africa, in particular, faces the challenge of limited job opportunities for Go developers. By contributing to open-source projects, we can showcase our skills to global audiences, helping to bridge that gap. Many speakers encouraged us to jump into open-source, not only to learn but to help build the Go ecosystem and demonstrate to employers what Go and African developers are capable of.&lt;/p&gt;

&lt;h2&gt;
  
  
  Networking
&lt;/h2&gt;

&lt;p&gt;One of the best parts of GopherCon Africa was the networking opportunities. The event provided a rare chance to meet veteran Gophers face-to-face, exchange ideas, and even brainstorm potential collaborations. There were students, junior developers, industry veterans, startup founders, and entrepreneurs each bringing a unique perspective. The diversity of attendees made the networking sessions even more valuable, as we got to learn from each other’s experiences, challenges, and successes. &lt;br&gt;
In one of the sessions, I had a conversation with a fellow developer about the increasing use of Go in fintech. We discussed how Go’s efficiency and scalability make it the go-to language for many fintech companies across Africa. This was just one example of the countless conversations that expanded my understanding of Go’s applications and the industries it’s shaping.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating More Awareness Around Go in Africa
&lt;/h2&gt;

&lt;p&gt;One thing that became clear during the event is that there is still much work to be done to create awareness around Go in Africa. Go is already making waves in industries like fintech, cloud computing, and telecommunications, but there are still many developers and companies who aren’t aware of its potential. The event made it clear that we need to continue advocating for Go, both through community efforts and by encouraging companies to adopt it in their tech stacks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Looking Forward: The Future of Go in Africa
&lt;/h2&gt;

&lt;p&gt;As GopherCon Africa came to a close, I left feeling both inspired and optimistic. There is a real sense that this is just the beginning. The language’s simplicity, performance, and scalability make it perfectly suited for solving many of the challenges African developers face. And with a growing community of passionate developers, I am confident that Go will continue to thrive here. &lt;br&gt;
We have the opportunity to drive Go adoption forward by contributing to open-source, building communities, and showing the world what African developers are capable of. I’m excited to see what the future holds, and even more excited to be part of it.&lt;/p&gt;

&lt;p&gt;To many more successful GopherCons in Africa! &lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
