<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[A pragmatic engineer blog, written by a human.]]></title><description><![CDATA[Senior backend engineer writing about distributed systems, observability, AI in software development, and pragmatic career growth for developers.]]></description><link>https://nguyengineer.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/64479ed989b09f5069ab7a18/c94b65b4-b044-45a7-91cd-e7b8e956a1b5.png</url><title>A pragmatic engineer blog, written by a human.</title><link>https://nguyengineer.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Mon, 05 Oct 2026 21:11:28 GMT</lastBuildDate><atom:link href="https://nguyengineer.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Scaling database practices]]></title><description><![CDATA[Scaling databases can feel like a scary topic. I'm not going to wait until I "know everything" to write about it though, there is always a higher mountain. This post is just my little hill of scaling ]]></description><link>https://nguyengineer.dev/scaling-database-practices</link><guid isPermaLink="true">https://nguyengineer.dev/scaling-database-practices</guid><category><![CDATA[postgres]]></category><category><![CDATA[database]]></category><category><![CDATA[scaling]]></category><category><![CDATA[backend]]></category><category><![CDATA[performance]]></category><category><![CDATA[architecture]]></category><dc:creator><![CDATA[Nguyen Engineer]]></dc:creator><pubDate>Wed, 08 Jul 2026 18:53:45 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/64479ed989b09f5069ab7a18/3509bbb5-0f93-4924-bd30-98d32ffaeb4b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Scaling databases can feel like a scary topic. I'm not going to wait until I "know everything" to write about it though, there is always a higher mountain. This post is just my little hill of scaling database practices, based on what I've seen so far.</p>
<p>I will try to cover this topic as much as possible, based on my current understanding of databases. I welcome all comments on what I'm missing or covered too shallowly. This post is also how I learn: writing it down forces me to understand each practice deeply, so I'll try to explain the why behind everything, not just the what.</p>
<p>I will pick a specific database to narrow down the scope, and I will guide you through a journey of scaling from an MVP to a large scale system. Think of this post as a guided tour rather than a textbook.</p>
<h2>What database are we scaling?</h2>
<p>There are many kinds of databases, but in general this will be the main database for any application, it could be SQL based or NoSQL based.</p>
<p>We will start with a naive setup: we have a SaaS app, a couple of users and a dozen tables to serve the main functionality, the web app directly opens connections, no pool, a few indexes.</p>
<p>At this point almost any database is capable, so we will pick Postgres, because it is open source, a multi purpose database, and has a lot of extensions in case we go into a niche.</p>
<p>Good enough at this stage:</p>
<ul>
<li><p>Single VM running Postgres</p>
</li>
<li><p>For monitoring, we rely on the VM's own monitoring: CPU, RAM, Disk usage. If CPU is constantly high, memory is full, or disk is about to run out, we know something is wrong and go investigate.</p>
</li>
<li><p>Automatic backup: daily or hourly disk snapshot. We should try to restore from the backup at least once to confirm it works.</p>
</li>
</ul>
<h2>When do I need a connection pool?</h2>
<p><code>ERROR 53300: Too many connections</code></p>
<p>I start to have many API calls and each API creates a new connection to the database, and eventually all the connection slots are exhausted. This error may have many root causes:</p>
<ul>
<li><p>Connections don't close properly</p>
</li>
<li><p>Long running transactions or queries hold the connection</p>
</li>
<li><p><code>max_connections</code> param set too low</p>
</li>
<li><p>Too many connections to the database (obviously)</p>
</li>
</ul>
<p>Why is Postgres so sensitive about this? Because Postgres forks a whole OS process for every connection. Each connection eats several MB of RAM and adds overhead to internal bookkeeping, even when it's idle. So "just raise <code>max_connections</code>" only delay the problem.</p>
<p>When the app and traffic are still small, the app's builtin connection pool like Node.js pg-pool or Go go-pg is usually enough. But once we scale the number of app instances or serverless functions all connecting to Postgres, the database starts to struggle with thousands of short lived connections. This is where we need a dedicated connection pooler like pgBouncer.</p>
<p><a href="https://planetscale.com/blog/scaling-postgres-connections-with-pgbouncer"><img src="https://cdn.hashnode.com/uploads/covers/64479ed989b09f5069ab7a18/70d3dfba-7510-4ca9-89ea-8f23cad7e154.png" alt="Source: https://planetscale.com/blog/scaling-postgres-connections-with-pgbouncer" style="display:block;margin:0 auto" /></a></p>
<p>So each app instance already has its local pool, then all the app instances connect to one or more pgBouncer instances. Each pgBouncer instance has a predefined, stable pool of connections to Postgres and multiplexes client requests onto them. That way, Postgres spends its valuable time running queries instead of forking and managing processes.</p>
<p>One thing to understand before deploying pgBouncer: the pool mode.</p>
<ul>
<li><p><strong>Session mode</strong>: a client keeps its server connection until it disconnects. Safe, but we don't gain much multiplexing.</p>
</li>
<li><p><strong>Transaction mode</strong>: a server connection is only borrowed for the duration of one transaction. This is where the big win is, but it breaks session-level features: <code>SET</code> commands, advisory locks, <code>LISTEN/NOTIFY</code>, and prepared statements (on older pgBouncer versions). We need to check our app and drivers against this list before rolling it out to production.</p>
</li>
</ul>
<p>Introducing pgBouncer is often the first scaling step for Postgres, long before we talk about sharding or more complex solutions.</p>
<h2>How do I know I need indexes?</h2>
<p>Answer: we need to measure.</p>
<p>Postgres constantly collects statistics about itself and exposes them as regular views we can query with plain SQL. No extra tool, no dashboard to install, we just connect with <code>psql</code> and <code>SELECT</code> from them. The database observes itself, we only need to ask.</p>
<p>The one to start with is <code>pg_stat_statements</code>. It's an extension that tracks every query shape that runs: how many times it was called, total time, average time, rows returned. "Shape" means parameters are normalized, so <code>WHERE id = 42</code> and <code>WHERE id = 99</code> are counted as one entry: <code>WHERE id = $1</code>. It ships with Postgres but is off by default (managed Postgres usually preloads it, then we just run <code>CREATE EXTENSION pg_stat_statements;</code>). Once it's on:</p>
<pre><code class="language-sql">SELECT query, calls, total_exec_time, mean_exec_time, rows
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;
</code></pre>
<p>Note the <code>ORDER BY</code>. There are two different questions hiding here:</p>
<ul>
<li><p><code>ORDER BY mean_exec_time</code> answers "which queries are slow?"</p>
</li>
<li><p><code>ORDER BY total_exec_time</code> answers "which queries hurt the database the most?" A 5ms query called 50,000 times per minute does far more damage than one slow 2-second query. Chasing total time instead of the slowest query is one of those small habits that separates guessing from measuring. This practice work on other database as well, not just Postgres.</p>
</li>
</ul>
<p><code>pg_stat_statements</code> has siblings, all queried the same way:</p>
<ul>
<li><p><code>pg_stat_activity</code>: what is running right now. First thing to check when "the database is stuck": long-running queries, or connections sitting in <code>idle in transaction</code> (bug: transaction opened, never committed).</p>
</li>
<li><p><code>pg_stat_user_tables</code>: per-table stats. A big table with a high <code>seq_scan</code> count is being full-scanned repeatedly, that's the index candidate.</p>
</li>
<li><p><code>pg_stat_user_indexes</code>: the opposite direction. An index with <code>idx_scan = 0</code> after weeks in production does nothing except slow down our writes. We should drop it.</p>
</li>
</ul>
<p>Then when a specific query is suspicious, prefix it with <code>EXPLAIN ANALYZE</code> and Postgres shows we the execution plan: sequential scan or index scan, and where the time actually went. So the troubleshooting loop looks like: <code>pg_stat_activity</code> (what's stuck now) -&gt; <code>pg_stat_statements</code> (what costs the most) -&gt; <code>EXPLAIN ANALYZE</code> (why this one is slow) -&gt; fix -&gt; measure again.</p>
<p>That said, indexes are one of the first things we should think about when designing tables. A good index can turn a painful full table scan into a quick lookup, and in practice it fixes most "my query is slow" issues, at least until the data grows big enough. Even when the table is still small, adding the right indexes early saves we from surprises later.</p>
<p>The trade off is always write performance. Every extra index makes inserts and updates a bit more expensive. For unique indexes it's even worse, because the database has to both find a place for the new data and check that the value is not already there.</p>
<p>Depending on the data and the query pattern, sometimes we don't need to index every row. We can use partial indexes with a condition. For example, in an <code>emails</code> table with <code>status = draft | live | editing</code> and <code>category = newsletter | transactional | notification</code>, maybe only non-draft emails are ever queried by users. In that case we can create an index only where <code>status != 'draft'</code>. Reads are faster because the index is smaller, and writes to draft emails stay cheap because they don't touch the index at all. One catch: the planner only uses a partial index when the query's <code>WHERE</code> clause matches the index condition, so <code>WHERE status = 'live'</code> works, but a query without a status filter won't.</p>
<p>If we really cannot avoid adding more indexes, that's usually a hint to also look at other options: improve the queries, archive old data out of the hot table, or later on use partitioning so each index only has to deal with a smaller slice of data.</p>
<p>If we use a managed Postgres, the service will usually surface slow queries and full‑table scans in its monitoring UI, but it won’t go as far as MongoDB Atlas and auto‑suggest exact indexes. In the Postgres world, index advice tends to live in separate tools (pganalyze, Datadog, pgHero...), while the core database only exposes the raw stats (<code>pg_stat_statements</code>, <code>pg_stat_user_tables</code>, <code>pg_stat_user_indexes</code>) and leaves the decision to us.</p>
<h2>What is vertical scaling?</h2>
<p>That is simply adding more CPU, RAM and Disk to the primary node until the curve between resources and performance doesn't align anymore.</p>
<p>This is why cloud providers make it easy to click and upgrade to a bigger instance. We just go from <code>4 vCPU / 16 GB RAM</code> to <code>16 vCPU / 64 GB RAM</code> and suddenly the system is faster. We can buy some time until we need another scaling strategy.</p>
<p>The trade offs:</p>
<ul>
<li><p>The pricing of the bigger instance, but we use this method for a while since hardware is cheap compared to engineering time. Note that we need at least one or two replicas (more on those later), so the price also multiplies.</p>
</li>
<li><p>The blast radius gets bigger: a larger backup takes longer to restore, a failover moves more traffic at once, and any mistake affects more data.</p>
</li>
<li><p>At some point, doubling CPU and RAM does not give us the same win anymore. I haven't operated at that ceiling myself, but the consensus is that vertical scaling alone eventually stalls, and we have to combine it with the other tools in this post: better indexes, partitioning, replicas, and in the extreme case, sharding.</p>
</li>
</ul>
<h2>What about caching?</h2>
<p>Before adding more database machinery, there is a cheaper question: does this read need to hit the database at all?</p>
<p>A cache like Redis (or even an in-process cache) in front of Postgres can absorb a lot of repetitive read traffic: rendered pages, user sessions, computed aggregates, reference data that changes once a day. The common pattern is cache-aside: try the cache, on miss read from Postgres and write back to the cache with a TTL.</p>
<p>The trade‑off is the famous hard problem: cache invalidation. From my experience, caches solve some problems but introduce others.</p>
<p>Once, we hit a serious production incident where data from one tenant was returned to another. All evidence pointed to Redis: the application logs and keys were correct, but the value that came back belonged to a different tenant. After restarting Redis the issue vanished and we were never able to reproduce it. My best guess is a hardware or memory corruption issue, but we never got a definitive root cause. I mention this not to blame Redis, but to show how caching can create subtle, high‑impact failures even when we think we are using it carefully.</p>
<p>Generally, anything with strong consistency requirements (like show the order user just created) should keep reading from the database. When it works, caching is often the cheapest way to buy time: it cuts read load enough so we don't need replicas or a bigger instance yet. But it is another moving part with its own consistency model, so it deserves as much design thinking as the database itself.</p>
<h2>Do I need partitioning?</h2>
<p>Before jumping to sharding, there is a practice we can apply first: partitioning. In Postgres, partitioning means one "logical" table is actually split into many smaller physical tables behind the scenes, but they all stay on the same server.</p>
<p>Take an example: I have a table <code>events</code> where the data naturally groups by <code>created_at</code>. I can create a partition:</p>
<pre><code class="language-sql">CREATE TABLE events_2026_07 PARTITION OF events
FOR VALUES FROM ('2026-07-01') TO ('2026-08-01');
</code></pre>
<p>From the application, I don't care about <code>events_2026_07</code> or <code>events_2026_08</code> at all:</p>
<pre><code class="language-sql">INSERT INTO events (user_id, created_at, payload)
VALUES (123, now(), '{ "action": "click" }');

SELECT * FROM events
WHERE created_at &gt; now() - interval '7 days'
  AND user_id = 123;
</code></pre>
<p>Postgres will:</p>
<ul>
<li><p>Route the <code>INSERT</code> into the right partition based on <code>created_at</code>.</p>
</li>
<li><p>For the <code>SELECT</code>, only touch the partitions that overlap "last 7 days" (this is called partition pruning).</p>
</li>
</ul>
<p>So yes, it's still one server, but it can be faster because Postgres skips work:</p>
<ul>
<li><p>It does not scan a huge single index; it works with smaller per-partition indexes.</p>
</li>
<li><p><code>VACUUM</code> / <code>ANALYZE</code> runs on smaller chunks.</p>
</li>
<li><p>When I want to delete old data, I don't run a giant <code>DELETE</code>, I can just: <code>ALTER TABLE events DETACH PARTITION events_2026_01; DROP TABLE events_2026_01;</code></p>
</li>
</ul>
<p>Typical patterns are:</p>
<ul>
<li><p>Time-based: one partition per day / week / month.</p>
</li>
<li><p>Tenant-based: one partition per customer or group of customers.</p>
</li>
</ul>
<p>It's all still one Postgres cluster, one primary, one connection string. Partitioning adds a bit of schema complexity, but compared to full sharding across many databases, it's a much smaller step and already helps with performance and data lifecycle.</p>
<p>But partitioning comes with trade offs: we are adding schema and operational complexity. In more detail:</p>
<ul>
<li><p>Partitions need to be created over time. But we should not do this manually, let's use an extension like <code>pg_partman</code> to automate it.</p>
</li>
<li><p>Migrations and tooling need to be partition-aware.</p>
</li>
<li><p>We need to pick a good partition key, and Postgres adds one extra rule: the partition key must be part of every primary key and unique constraint on the table.</p>
<p>In practice, that means we should not partition directly on a high cardinality column like <code>created_at timestamptz</code> and then use <code>(id, created_at)</code> as the primary key. It works, but it's awkward to query and doesn't give us a clean "id is unique" guarantee anymore.</p>
<p>A more practical pattern is to add a coarser key and partition on that:</p>
<pre><code class="language-sql">CREATE TABLE events (
  id          bigint,
  user_id     bigint,
  bucket_date date NOT NULL,
  created_at  timestamptz NOT NULL,
  payload     jsonb
) PARTITION BY RANGE (bucket_date);

ALTER TABLE events
  ADD PRIMARY KEY (id, bucket_date);
</code></pre>
<p>Now <code>bucket_date</code> is the partition key, and <code>(id, bucket_date)</code> is what Postgres uses to enforce global uniqueness across partitions. If the application really wants to treat plain <code>id</code> as the identifier and query with <code>WHERE id = ...</code>, we either add a separate unique/index on <code>id</code>, or accept that "id is globally unique" is enforced by our code, not by the database.</p>
</li>
</ul>
<p>Partitioning becomes interesting when we deal with data on a hot disk (NVMe) and cold disk (SSD, HDD): the latest partition stays on the hot disk for frequent queries, the older partitions move to the cold disk to save cost, but all the data stays in the same database for the occasional query. In practice it looks like this:</p>
<ol>
<li><p>Mount different disks on the machine, for example:</p>
<ol>
<li><p><code>/mnt/nvme_hot</code> -&gt; fast, expensive</p>
</li>
<li><p><code>/mnt/ssd_cold</code> -&gt; slower, cheaper</p>
</li>
</ol>
</li>
<li><p>Tell Postgres "this folder = this tablespace":</p>
<pre><code class="language-sql">CREATE TABLESPACE hot_ts LOCATION '/mnt/nvme_hot';
CREATE TABLESPACE cold_ts LOCATION '/mnt/ssd_cold';
</code></pre>
</li>
<li><p>When creating a partition, put it on the right tablespace:</p>
<pre><code class="language-sql">CREATE TABLE events_2026_07 PARTITION OF events
FOR VALUES
FROM ('2026-07-01') TO ('2026-08-01') TABLESPACE hot_ts;
</code></pre>
</li>
<li><p>Now all the files for <code>events_2026_07</code> live physically on the NVMe disk. When that partition becomes "cold", move it: <code>ALTER TABLE events_2026_07 SET TABLESPACE cold_ts;</code> Postgres will copy the table's files from <code>/mnt/nvme_hot</code> to <code>/mnt/ssd_cold</code> under the hood.</p>
</li>
</ol>
<p>Two honest caveats on this practice: it only works on self-hosted Postgres, managed services like RDS or Cloud SQL don't let we mount disks or define custom tablespaces. And <code>SET TABLESPACE</code> takes an ACCESS EXCLUSIVE lock for the whole copy, so on a big partition that is real downtime.</p>
<p>That's enough about partitioning. In practice, most teams try everything inside a single Postgres cluster before jumping to sharding. Partitioning is already a noticeable step up in complexity, but it still keeps one primary, one backup story, one connection string.</p>
<h2>When do I need replicas?</h2>
<p>The first obvious reason to add replicas is to reduce read load on the primary. At some point one node is doing too much: it has to handle every write and also every read (dashboards, exports, background jobs, admin tools). With one or more replicas we can send some of the read traffic away from the primary and keep it focused on writes and the most critical queries.</p>
<p>But replicas are not only about load, they are also a basic high availability (HA) strategy. If the primary dies, requests to that node will fail or stall until something promotes a replica and points traffic there. In practice we use extra tools (Patroni, Stolon, cloud control planes) that run on top of Postgres and use a consensus store like etcd or Consul (which internally run algorithms like Raft) to decide which replica becomes the new primary, and to deal with the old primary when it comes back up. This is a sophisticated problem and it isn't worth solving ourselves when the industry already has the standard, or we simply use managed Postgres from a cloud provider.</p>
<p>Of course, replicas come with trade offs. Replication is asynchronous by default, so there is always some lag. If a user writes data and then we immediately read from a replica, they might not see their change yet. So we need to be careful which queries are allowed to hit replicas</p>
<ul>
<li><p>Strong consistency paths (like "show me the order I just created") should read from the primary</p>
</li>
<li><p>Things that can be a few seconds behind (reports, analytics pages, background jobs) can go to replicas. This is the "read your own writes" problem.</p>
</li>
</ul>
<p>This is where the OLTP / OLAP split appears. OLTP is the normal product traffic: small, focused reads and writes on fresh data. OLAP is the heavy stuff: big scans and aggregations. If we run OLAP queries directly on the primary, they will compete with OLTP and affect the performance. A common pattern is: primary for OLTP, one small replica ready for failover, and maybe one "analytics" replica that serve the requests from dashboards or batch jobs. Even then, replicas still share the same schema and indexes as the primary, so if the analytics workload grows too much we usually push it into a separate OLAP store via CDC.</p>
<h2>When do I consider sharding?</h2>
<p>If the system is multi-tenant or the data can be grouped by some clear key, we can start thinking of sharding. Sharding basically means instead of one big Postgres cluster, we split the data across multiple databases, usually by a shard key like <code>tenant_id</code> or <code>region</code>.</p>
<p>A simple example: each tenant (or group of tenants) goes to its own database. All the tables look the same, but tenant A lives on shard 1, tenant B lives on shard 2, and so on. From the app side, we either have a small routing layer, or the app itself decides which connection string to use based on the shard key. The main win is isolation: a noisy tenant or a huge customer only overloads its own shard, not everybody else. Storage and write load are also spread across machines instead of hammering a single primary.</p>
<p>One warning on picking the shard key: it should spread the writes. Sharding by time (one shard per day or month) sounds tempting but it's an anti-pattern for write scaling, all new writes hammer the newest shard while the old shards sit idle. Shard by tenant or a hash of some ID, and partition by time <em>within</em> each shard if needed.</p>
<p>Of course, the trade offs are much bigger than partitioning. Now we don't just manage one Postgres cluster, we manage many: more backups, more replicas, more monitoring, more things to upgrade. Queries that need data from multiple shards become more annoying: either we avoid them, or we have to fan out and aggregate in the app or some separate job. Moving tenants between shards and keeping them balanced is also extra work. We trade operational risk for performance.</p>
<p>Because of that, we should only start to seriously consider it when:</p>
<ul>
<li><p>A single well-tuned Postgres cluster (with connection pooling, good indexes, partitioning, and vertical scaling) is still not enough for the write/load pattern, or</p>
</li>
<li><p>We have a couple of big tenants that clearly deserve their own hardware so they don't hurt everyone else.</p>
</li>
</ul>
<p>When we reach that stage, we should be ready to live with the operational cost of many Postgres clusters.</p>
<h2>CDC &amp; ETL to OLAP / other stores</h2>
<p>Postgres records every change in its write-ahead log (WAL) before applying it. Physical replication (the replicas above) works by shipping this WAL to standby servers. Change data capture (CDC) reads the same WAL but through a different mechanism, logical decoding, which turns the low-level changes back into a stream of row-level insert/update/delete events. Once people had that stream, they found many uses for it: event driven architecture, the outbox pattern, notifications, and feeding OLAP stores.</p>
<p>We just subscribe to the change log. Tools like Debezium or cloud-native services read the WAL from Postgres and turn it into an event stream of inserts/updates/deletes.</p>
<p>Once we have that stream, we can build an ETL pipeline:</p>
<ul>
<li><p>Extract: listen to the change events coming from Postgres (for example via Debezium into Kafka).</p>
</li>
<li><p>Transform: clean up or enrich the events in a stream processor or simple workers (cast types, denormalise some fields, join with reference data).</p>
</li>
<li><p>Load: write the transformed data into a store that is better for analytics: a columnar warehouse, a time-series database, a search index, etc.</p>
</li>
</ul>
<p>The OLTP database stays focused on small, transactional queries. The OLAP side (warehouse, time-series, whatever we pick) gets a copy of the data that is usually a few seconds behind, but is shaped and indexed for heavy reads.</p>
<p>CDC isn't free, the trade offs:</p>
<ul>
<li><p>We add more moving parts. Instead of "app → Postgres → dashboards", now we have app → Postgres → WAL → CDC connector → message bus (Kafka, Kinesis) → ETL jobs → warehouse / OLAP store → dashboard. Every box can break, lag, or need upgrades. Operating this pipeline is a real ongoing cost.</p>
</li>
<li><p>One subtle risk with CDC is how it uses replication slots. A logical consumer (like Debezium) tells Postgres "keep WAL from this position onward for me", and Postgres will not discard any WAL segments that this slot has not acknowledged yet. If the consumer falls behind badly or is quietly shut down, that slot never advances, so every new write generates more WAL that cannot be cleaned up. Over time those files pile up on disk. In the worst case they fill the volume and the primary cannot write new WAL or commit transactions, effectively taking it down. The practical fix is operational: monitor replication slot lag as a first‑class metric (for example, <code>pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn)</code> from <code>pg_replication_slots</code>), alert when a slot stops moving, and drop abandoned slots so Postgres can safely recycle old WAL.</p>
</li>
<li><p>Our system turns from consistent to eventually consistent.</p>
</li>
<li><p>Schema changes become harder to manage: one change in the Postgres schema needs updates through the whole pipeline.</p>
</li>
<li><p>We increase the cost of every write: replicating data, writing logs, computation in the ETL pipeline.</p>
</li>
<li><p>We face the general problems of distributed systems: handling duplicates, retries, backfills, idempotent workflow design, message ordering, reprocessing events after fixing a bug in the ETL (the event sourcing problem).</p>
</li>
</ul>
<h2>What about operational concerns?</h2>
<p>All of the scaling practices above only work if we can see what is going on in the database and change things safely. The basic concerns are:</p>
<h3>Observability</h3>
<p>We need at least charts, logs, and alerts around some key metrics: connections, IOPS, CPU, RAM, disk usage, replication lag, and slow queries. With a bare metal Postgres setup, the loop is: running Postgres -&gt; exporter process -&gt; expose HTTP endpoint -&gt; Prometheus periodically calls that endpoint -&gt; stores time series data -&gt; shows charts. Then we set up alert rules like:</p>
<ul>
<li><p>CPU &gt; 80% for 5 minutes, fire alert.</p>
</li>
<li><p>Free disk &lt; 20%, fire alert.</p>
</li>
<li><p>Replication lag &gt; 30 seconds for 2 minutes, fire alert...</p>
</li>
</ul>
<p>Managed Postgres does the same thing under the hood, it just hides the exporter + Prometheus bits and gives you "CPU graph + alarms" as a service.</p>
<p>By sending alerts to Slack or PagerDuty, we can detect issues early and start thinking about the scaling practices.</p>
<p>Observability brings up capacity planning. Instead of waiting for "database is down", we watch leading indicators: CPU trend, connection count, table and index size, replication lag, storage usage. When the curves start to bend, we plan changes (new indexes, bigger boxes, replicas, partitioning) before we hit the wall.</p>
<h3>Autovacuum and bloat</h3>
<p>This deserves its own mention because it's one of the most common ways a real Postgres install falls over at scale.</p>
<p>Postgres uses MVCC (Multi-Version Concurrency Control): an <code>UPDATE</code> or <code>DELETE</code> doesn't remove the old row, it marks it dead and leaves it in place. The dead rows (bloat) are cleaned up later by autovacuum. On a quiet database the defaults are fine. On a write-heavy table, autovacuum can fall behind: tables and indexes grow with dead space, scans get slower, and everything looks like a mystery performance problem even though the indexes are "right".</p>
<p>Two things to do:</p>
<ul>
<li><p>Watch bloat and autovacuum activity (<code>pg_stat_user_tables</code> shows dead tuples and last vacuum times). Tune autovacuum to be more aggressive on hot tables, the default settings are conservative.</p>
</li>
<li><p>Know that transaction ID wraparound exists: if vacuum is prevented from running long enough (for example by a forgotten long-running transaction or an abandoned replication slot), Postgres will eventually force a shutdown to protect the data. It's rare, but we should monitor for it: watch <code>n_dead_tup</code> in <code>pg_stat_user_tables</code>, and never let transactions or replication slots sit idle for days.</p>
</li>
</ul>
<h3>Safe schema changes</h3>
<p>A schema change on production can require long locks and downtime, we can't just simply <code>ALTER TABLE</code>. We need a better way: add columns in a compatible way -&gt; backfill in small batches -&gt; only drop old columns when nothing reads them anymore.</p>
<p>We also need to propagate the change through the ETL pipeline if any, which means it isn't just a technical change: we need to align the schema change with other teams, make sure all of them follow the new schema, then drop the old one. This sounds obvious, but in a big company, the communication issue usually leads to more problems than the technical one.</p>
<h3>Backup and restore tests</h3>
<p>You may have heard the <a href="https://blog.ishosting.com/en/why-backups-matter">stories</a> of big companies that did backups frequently but never tried to restore, and when they had to, it didn't work and became a disaster. Recently I learned about 2 numbers that indicate a good backup strategy: RPO and RTO.</p>
<ul>
<li><p>RPO (Recovery Point Objective): How much data can we afford to lose? For example 15 minutes, then we need a base backup and WAL archives to have point in time recovery.</p>
<ul>
<li><p>Point-in-time recovery is about being able to restore your database to an exact moment in the past, not just to when a backup file was taken. In Postgres terms, it works like this in practice:</p>
<ul>
<li><p>You take a base backup (a full snapshot of the database at some time, e.g. 02:00).</p>
</li>
<li><p>You also keep all WAL segments that record every change after that backup.</p>
</li>
<li><p>If something bad happens at 10:37 (buggy deploy, <code>DELETE FROM users</code>, disk crash), you:</p>
</li>
<li><p>Restore the base backup from 02:00,</p>
</li>
<li><p>Then replay WAL forward until just before the bad thing happened, for example 10:36:55. That's called Point-In-Time Recovery (PITR): "give me the database as it looked at 2026-07-08 10:36:55".</p>
</li>
</ul>
</li>
</ul>
</li>
<li><p>RTO (Recovery Time Objective): How long can we afford to be down while restoring the data? For example, if our RTO is 30 minutes, but a full restore from S3 to a fresh VM already takes 45 minutes, then we need to review our backup plan. One way to know is to actually do a restore from time to time:</p>
<ul>
<li><p>Take one of our real backups.</p>
</li>
<li><p>Spin up a new Postgres instance in a separate environment.</p>
</li>
<li><p>Restore the backup and replay WAL to some point in time.</p>
</li>
<li><p>Measure how long it takes until the database is ready to accept connections.</p>
</li>
</ul>
</li>
</ul>
<p>That test tells us the real RTO number. If the number is too big, we either change the backup strategy (more frequent base backups, faster storage) or we accept that our RTO is worse than we wanted. For small side projects a daily snapshot is probably fine. For serious systems we almost always need continuous archiving and regular restore drills.</p>
<h2>The iterative mindset</h2>
<p>Scaling is not a one time project. We measure, notice the potential problem, make the change, measure again and keep going.</p>
<p>Answering a scaling problem requires understanding the problem first. Let me play out how this looks in an Q&amp;A style conversation, because that back-and-forth of questions <em>is</em> the method.</p>
<p><strong>Question: We need to write a billion data points and adding an index will slow the writes. We need to keep the writes scalable, what should we do?</strong></p>
<p>Before answering, I need to question back. What is the time range for that billion data points, per day, per hour, per minute? How big is a record and what is in it? What is the current schema and the existing indexes? And why do we need the new index?</p>
<p><strong>Q: Say it's a billion per day.</strong></p>
<p>That's around 12K writes per second on average. A well-tuned single Postgres with vertical scaling and batched inserts can likely handle it. No exotic architecture needed yet.</p>
<p><strong>Q: What if it's closer to a billion per hour?</strong></p>
<p>Then the next question is: do we need ACID on this data, or can we accept eventual consistency? If we can accept it, put a high throughput message queue like Kafka in front to buffer the spikes, and insert into the database gradually through the queue. If we truly need transactional consistency at that rate, we shard, and the shard key must spread the writes: shard by tenant or a hash of some ID, never by time. Time-based sharding means every new write hammers the newest shard while the others sit idle. Partition by time within each shard instead.</p>
<p><strong>Q: And the index we wanted to add?</strong></p>
<p>What does it's for, OLTP or OLAP? If it's a non-negotiable OLTP index, consider a partial index to reduce the number of rows it covers, and archive or partition old, cold data so the index only covers the current working set. If it's for OLAP queries, we don't optimize heavy aggregations on the OLTP tables, we can use CDC / ETL to push the data into a warehouse or OLAP-friendly store and index it there.</p>
<p><strong>Q: Does the shape of the data change the answer?</strong></p>
<p>Yes. If the data is mostly unstructured documents, we can store it as JSONB in Postgres, but that should be treated as a convenience feature, not a full replacement for a document database. For heavy document workloads, purpose-built engines like MongoDB or Couchbase are usually a better fit.</p>
<p>If the data is structured, ask how it is queried and how many fields are repetitive versus unique per row. A high ratio of repeating values plus heavy analytical queries points to a columnar or warehouse-style store such as ClickHouse, Amazon Redshift, or Google BigQuery, which can compress and scan those columns much more efficiently. Postgres is a great fit for structured, transactional data where we care about ACID; for large unstructured or purely analytical workloads, a document or columnar database will usually be the better primary home.</p>
]]></content:encoded></item><item><title><![CDATA[How I built and scaled real‑time chat app from 2 to millions of users — without a rewrite?]]></title><description><![CDATA[Real-time is one of those features that demos in an afternoon and humbles you for a month. Open a WebSocket, push a message, watch it appear on the other screen — done, right? That's the part that's e]]></description><link>https://nguyengineer.dev/how-i-built-and-scaled-real-time-chat-app-from-2-to-millions-of-users-without-a-rewrite</link><guid isPermaLink="true">https://nguyengineer.dev/how-i-built-and-scaled-real-time-chat-app-from-2-to-millions-of-users-without-a-rewrite</guid><category><![CDATA[websockets]]></category><category><![CDATA[Go Language]]></category><category><![CDATA[realtime apps]]></category><category><![CDATA[kafka]]></category><category><![CDATA[postgres]]></category><category><![CDATA[cdc]]></category><category><![CDATA[debezium]]></category><category><![CDATA[Centrifugo]]></category><category><![CDATA[scaling]]></category><dc:creator><![CDATA[Nguyen Engineer]]></dc:creator><pubDate>Tue, 30 Jun 2026 13:14:23 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/64479ed989b09f5069ab7a18/da5d321c-14f2-4ff7-819f-9193d9b223e6.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Real-time is one of those features that demos in an afternoon and humbles you for a month. Open a WebSocket, push a message, watch it appear on the other screen — done, right? That's the part that's easy. The part that makes it <em>engineering</em> is what happens after the demo: when the process dies mid-send, and when the two users in your test become two million concurrent conversations.</p>
<p>I built a small project to take both of those seriously — an <mark class="bg-yellow-200 dark:bg-yellow-500/30">ephemeral one-on-one chat.</mark> A host opens a room, a guest joins, the two talk in real time, and the moment either leaves, the room and every message in it are deleted. No history, no archive. The feature is small on purpose. The architecture is not, and that's the point of this post<mark class="bg-yellow-200 dark:bg-yellow-500/30">: the same codebase that serves two users is designed to serve millions, and the path between those two numbers is adding boxes, not rewriting code.</mark> I'm confident this design carries a million concurrent chats. Here's the reasoning.</p>
<h2>The naive design dies twice</h2>
<p>Here's the version almost everyone writes first:</p>
<ol>
<li><p>Save the message to the database.</p>
</li>
<li><p>Push the message to the connected clients.</p>
</li>
</ol>
<p>It works every time you test it, and it dies for two independent reasons.</p>
<p><strong>It dies for correctness</strong>. The gap between step 1 and step 2 is a crash-shaped hole.</p>
<blockquote>
<p><strong>The dual-write problem:</strong> when one operation updates two systems that don't share a transaction (a database and a message broker), any crash <em>between</em> the writes leaves them inconsistent, with no clean way to recover.</p>
</blockquote>
<p>Crash after the commit but before the push, and the message is saved but never delivered. Push first instead, and a crash leaves it delivered but never saved — gone on the next page load. No ordering of two independent writes survives a crash in the middle.</p>
<p><strong>It also dies for scale</strong>, and this is the one I actually care about. In the naive design the request thread does the fanout — it holds the WebSocket connections and pushes to each recipient inline. That couples two things that must never be coupled at scale:</p>
<ul>
<li><p>Request latency is now proportional to recipient count. Sending into a busy room, or broadcasting the lobby's room list to every idle user, makes the <em>sender's</em> request slow. Fanout size should never be in the critical path of a write.</p>
</li>
<li><p>The server is now stateful. It holds connections in memory, so you can't just add a second box — connections for a conversation might live on a different instance than the write. You're one node, or you're building sticky sessions and a custom cross-node fanout. You've painted yourself into a single-server corner on day one.</p>
</li>
</ul>
<p>You can't patch your way out of either of these. You design your way out.</p>
<h2>One write, then get out of the way</h2>
<p>The fix for both problems is the same move: the request does exactly one thing — a single database transaction — and then returns. It never touches the broker, never holds a connection, never fans out.</p>
<p>That's the <mark class="bg-yellow-200 dark:bg-yellow-500/30">transactional outbox</mark> pattern (well documented by Chris Richardson and the microservices community). In one transaction you write <em>two</em> facts to the database: the business row, and a row in an <code>outbox</code> table describing what to publish and to whom. One transaction, one source of truth — either both land or neither does.</p>
<pre><code class="language-go">func (o *messageUsecase) Create(ctx context.Context, msg string, publicRoomId, publicUserId uuid.UUID) (model.Message, error) {
	var createdMsg model.Message
	errCreate := o.txManager.UseTx(ctx, func(ctx context.Context) (err error) {
		// ... resolve room and user ...

		createdMsg, err = o.messageRepo.Create(ctx, newMsg)   // (1) the message
		if err != nil {
			return
		}

		// ... work out who needs to receive it ...

		// (2) the delivery intent — same transaction, no broker call
		err = o.outboxUsecase.PublishMessageToUsersInRoom(ctx, createdMsg, user, usersToPublish)
		return
	})
	return createdMsg, errCreate
}
</code></pre>
<p>Look at what this buys at scale. The request does bounded, local work — render the message for each recipient (O(N) CPU, but cheap and never more than the people in a room) and a single batched insert (O(1) round-trips). It holds no WebSocket connections and performs no broadcast fanout itself, so the app tier stays stateless and scales sideways. The expensive, unbounded part — actually reaching everyone — happens asynchronously downstream of Kafka, where it can be absorbed and parallelized. And the one path where fanout really is large — broadcasting the lobby's room list — is a single outbox row addressed to many channels, so even that stays O(1) on the write.</p>
<p>And because the request never holds a connection, the app tier is completely stateless. Every instance does nothing but write to Postgres and return. That single property is what lets you put fifty app servers behind a load balancer with no coordination between them. Stateless is the precondition for horizontal scale, and the outbox is what makes the app stateless.</p>
<h2>Why Kafka — the part that actually scales to millions</h2>
<p>Once the app just records intent and walks away, something has to turn those <code>outbox</code> rows into delivered messages. That something is a change-data-capture pipeline, and the choice of Kafka in the middle is the deliberate scale decision of the whole system.</p>
<img src="https://cdn.hashnode.com/uploads/covers/64479ed989b09f5069ab7a18/ad6471ad-b31f-4698-b170-a8d152f1fa4d.png" alt="" style="display:block;margin:0 auto" />

<p>Every box on that diagram scales on its own axis, and Kafka is why. Specifically:</p>
<ul>
<li><p>Throughput through partitioning. Kafka is a distributed, partitioned, replicated log. You scale write and read throughput by adding partitions and brokers — there's a well-trodden path from thousands to millions of messages per second. The delivery tier's ceiling is Kafka's ceiling, and Kafka's ceiling is very far away.</p>
</li>
<li><p>A durable buffer, so spikes don't cascade. When a traffic spike hits, or Centrifugo briefly falls behind, messages queue <em>durably in the log</em> instead of backing up into the app or getting dropped. Kafka turns a spike into a slightly longer log, not an outage. That backpressure boundary is exactly what keeps a real-time system standing during the moments that matter.</p>
</li>
<li><p>Parallel consumers via consumer groups. Centrifugo reads as a consumer group; partitions distribute across its instances. More connected users → more Centrifugo nodes → more partitions consumed in parallel. The WebSocket tier scales independently of the app tier and the database.</p>
</li>
<li><p>Per-room ordering for free. Every outbox row is stamped with its room id as the partition key:</p>
<pre><code class="language-go">// partitionId = message.RoomId
o.outboxRepo.CreatePublishOutbox(ctx, channel, renderedMessage, message.RoomId)
</code></pre>
<p>That's the seam that lets a single room's messages stay strictly ordered on one partition while millions of <em>other</em> rooms spread across the cluster for throughput. Ordering and parallelism usually pull against each other; partition-by-room is how you get both.</p>
</li>
<li><p>New consumers cost the write path nothing. The log is retained, so the day you want moderation, search indexing, analytics, or an audit trail, you attach another consumer group and replay. At a million chats you <em>will</em> want those — and none of them require touching the code that sends a message.</p>
</li>
</ul>
<p>This is also why I use CDC instead of polling the outbox table. Polling works at small scale, but at large scale it puts a relentless query load on the database and floors your latency at the poll interval. Debezium reads Postgres' write-ahead log — the database already wrote it, so capture is nearly free and nearly instant. It's the version of "drain the outbox" that doesn't fight you as the numbers grow.</p>
<h2>Ephemerality is a scaling feature, not just a product one</h2>
<p>The obvious objection to the diagram above: <em>Postgres is your single stateful core — isn't it the bottleneck?</em> It's the right question, and the design has two answers.</p>
<p>First, rooms are independent. Two conversations share nothing, which makes the room id a natural shard key. Past the ceiling of one primary, you partition or shard by room and the write load spreads cleanly, because nothing needs to join across rooms.</p>
<p>Second — and this is the part I like most — the data is ephemeral by design, and ephemerality bounds the working set. When a conversation ends, the room and its messages are deleted. The database holds only <em>active</em> chats, never the full history of everything ever said. A product with a million concurrent chats stores a million chats' worth of rows, not ten years of accumulated archive. The one stateful tier stays small precisely because nothing is allowed to persist. The feature that makes the product <em>feel</em> private is the same property that keeps the core scalable.</p>
<h2>HTML over the wire — and why I didn't reach for React</h2>
<p>The default for an app like this in 2026 is a React SPA talking to a JSON API. It's a fine architecture for some products. It also drags in a specific set of bugs that have nothing to do with your feature and everything to do with the <em>shape</em> of the design — and I've debugged all of them more than once:</p>
<ul>
<li><p>Two renderers, one truth. The server owns the data; the client owns the markup. So display logic — formatting, "show this badge when…", "hide the delete button unless you're the host" — gets written twice, in two languages, and drifts. The two halves of your app slowly start to disagree.</p>
</li>
<li><p>The state-sync problem. This is the big one. The client keeps a <em>copy</em> of server state and has to keep it fresh: cache invalidation, stale reads, optimistic updates that need rolling back when the server says no. You've created a distributed-state problem inside your own UI. Half of what React-Query and Redux do exists to manage a problem you only have because you chose to duplicate the state.</p>
</li>
<li><p>Contract drift. Rename a field on the server and a cached SPA bundle in someone's browser breaks. Now you're versioning APIs and writing contract tests to police an agreement between two things you wrote yourself.</p>
</li>
<li><p>Permissions in the wrong place. "Hide the button unless you're the host" is a client check — but the client can't be trusted, so the real rule has to live on the server too. Put it only on the client and it's a security hole; put it in both and it drifts.</p>
</li>
<li><p>Round-trip waterfalls. A screen needs data from three endpoints, so it's three sequential fetches and three spinners — or a bespoke per-screen endpoint, or GraphQL and its own complexity tax.</p>
</li>
</ul>
<p>Every one of these is a <em>consequence of splitting the view across the wire as data</em>. So I didn't split it. The server is the single source of truth and it sends rendered HTML — the <em>hypermedia-driven</em> approach, <em>HTML over the wire</em>, revived by <a href="https://htmx.org">htmx</a> and Carson Gross's <em>Hypermedia Systems</em>, DHH's Hotwire (literally HTML Over The WIRE), and Phoenix LiveView. The browser's only job is to drop the HTML into the page. There's no client copy of state to sync, no JSON contract to drift, and permissions are decided in the one place that also renders the markup.</p>
<p>I wanted to prove it holds <em>all the way through</em> a real-time system, so even the outbox payload carries HTML, not a message object:</p>
<pre><code class="language-go">renderedMessage, err := o.roomPresenter.RenderMessage(ctx, message, sender, receiver)  // -&gt; HTML string
_, err = o.outboxRepo.CreatePublishOutbox(ctx, channel, renderedMessage, message.RoomId)
</code></pre>
<p>The thing travelling Debezium → Kafka → Centrifugo → the socket is a <code>&lt;div&gt;</code>, not a <code>{}</code>. And the same <code>ChatMessage</code> templ component renders in all three places a message can appear — the initial page load, the sender's HTTP response, and the live socket push:</p>
<pre><code class="language-go">// the sender's own message, rendered straight into the HTTP response
err = messageComponent.ChatMessage(newMsg, user, user).Render(ctx, w)
</code></pre>
<p>One definition of what a message looks like, three delivery mechanisms, zero contract to keep in sync. It composes with the scale story, too: the delivery tier stays <em>dumb</em> — Kafka and Centrifugo shovel an opaque payload with no schema or logic in the hot path — while the one smart step, rendering, happens on the stateless app tier you can add boxes to.</p>
<h3>The trade-off</h3>
<p>None of this is free, and it's the wrong choice for some apps:</p>
<ul>
<li><p>You ship more bytes per update — HTML is heavier than JSON for the same data (though gzip narrows it, and you're not also shipping a megabyte of framework).</p>
</li>
<li><p>The server does the rendering, spending CPU the client would otherwise spend. Cheap and cacheable here, but real.</p>
</li>
<li><p>It's a poor fit for genuinely client-heavy UIs — offline-first, local computation, canvas-style interactivity (Figma, Maps, a spreadsheet). Those <em>need</em> client state; hypermedia would fight them.</p>
</li>
<li><p>There's no free JSON API for, say, a future native mobile client. If I ever need one, I add it then — I'm not paying for it now.</p>
</li>
</ul>
<p>I took those costs on purpose, because this app sits exactly where hypermedia wins: it's server-state-driven — the UI is a reflection of rooms and messages that live in the database, not a client-side computation — and it's built by a person. Trading flexibility I don't need for the elimination of an entire second codebase, a state-sync layer, and whole categories of drift bugs is not a close call. The best code is the code I didn't have to write to keep two copies of the truth agreeing.</p>
<h2>The seams that keep each tier swappable</h2>
<p>A system with this many collaborators — database, transaction, broker, renderer, object store — only stays sane if the dependencies point inward and are expressed as interfaces. The use cases depend on <code>RoomRepo</code>, <code>TxManager</code>, not on pgx or Centrifugo. That's what let me unit-test the transactional logic with hand-rolled fakes and no database at all, and it's what lets any one tier be replaced as the scale story evolves.</p>
<p>My favourite small seam is how a transaction crosses layers without leaking its <em>type</em> everywhere. The <code>TxManager</code> stashes the live transaction in the context; every repository quietly asks "is one in flight? use it; otherwise use the pool":</p>
<pre><code class="language-go">func GetTxIfAny(ctx context.Context, p *pgxpool.Pool) Querier {
	tx, ok := ctx.Value(helper.TxKey).(pgx.Tx)
	if ok {
		return tx
	}
	return p
}
</code></pre>
<p>A repository doesn't know whether it's inside <code>UseTx</code>. The use case owns the transactional boundary; the repositories just honour it. Ten lines now, instead of threading a <code>tx</code> argument through every signature later.</p>
<h2>What's still the frontier</h2>
<p>Confidence isn't pretending the work is finished — it's knowing exactly where the next problems live and seeing that the architecture already has room for them:</p>
<ul>
<li><p>Delivery is at-least-once, not exactly-once. A consumer can replay after a crash, so the client needs to de-duplicate by message id. That's a known, bounded piece of work — and at-least-once is the <em>correct</em> default; it's the guarantee that never loses a message.</p>
</li>
<li><p>Postgres sharding is real work past a point. The shard key (room id) and the bounded, ephemeral working set are already in place; turning the crank is an infrastructure step, not a re-architecture.</p>
</li>
<li><p>Identity is a cookie, not real auth. Fine for a demo, first on the list for anything real.</p>
</li>
</ul>
<p>None of these change the shape of the system. They're the next boxes, not the next rewrite — which is the whole claim.</p>
<h2>The takeaway</h2>
<p>Anyone can wire up a WebSocket for two users. The design question is whether those two users and two million share a codebase or a tombstone. This one shares a codebase, because every decision was made against the larger number: the request does one transaction and gets out of the way, so the app tier is stateless and scales sideways; Kafka absorbs the fanout, buffers the spikes, orders each room, and feeds consumers you haven't written yet; ephemerality keeps the one stateful core small; and the smart rendering happens once, far from the tier that has to reach everyone.</p>
<p>The WebSocket was the easy afternoon. An architecture that grows by adding boxes instead of starting over — that's the month. And that month is the work that's still ours.</p>
<h3>Public GitHub repo for the code</h3>
<p><a href="https://github.com/finnng/ephemeral-chat">https://github.com/finnng/ephemeral-chat</a></p>
<h3>Some screenshots</h3>
<ul>
<li>The main screen, every users will see the lobby to select the room or create a chat room to wait for the other to join</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/64479ed989b09f5069ab7a18/90434436-b6ec-40e6-835c-b7a3fcb58a75.png" alt="" style="display:block;margin:0 auto" />

<ul>
<li>The other user will see the room in the lobby immediately.</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/64479ed989b09f5069ab7a18/d61458dc-9cfe-4ed8-a5a1-b9a27a4329b2.png" alt="" style="display:block;margin:0 auto" />

<ul>
<li><p>Then they can click to the room name to join and start the chat. The room now will disappear from the lobby. Only two of them can chat</p>
<img src="https://cdn.hashnode.com/uploads/covers/64479ed989b09f5069ab7a18/8a526ccb-91b9-4fc6-8a1c-b2faf8f8e9a6.png" alt="" style="display:block;margin:0 auto" />
</li>
<li><p>The view from the host</p>
<img src="https://cdn.hashnode.com/uploads/covers/64479ed989b09f5069ab7a18/196f3336-2382-410a-be47-10704a78aae7.png" alt="" style="display:block;margin:0 auto" /></li>
</ul>
<p>It was fun building this project. I'm thinking of hosting it somewhere to let people come to chat just for fun ;)</p>
]]></content:encoded></item><item><title><![CDATA[We won Honestbee Logistics Challenge 2016]]></title><description><![CDATA[I usually go out and join tech events in the city. I’ve joined several of them, but the most memorable was the event where my friends and I won first prize by solving a delivery optimization problem. ]]></description><link>https://nguyengineer.dev/we-won-honestbee-logistics-challenge-2016</link><guid isPermaLink="true">https://nguyengineer.dev/we-won-honestbee-logistics-challenge-2016</guid><category><![CDATA[logistics]]></category><category><![CDATA[algorithm]]></category><category><![CDATA[BFS]]></category><category><![CDATA[Route Optimization]]></category><category><![CDATA[hackathon]]></category><dc:creator><![CDATA[Nguyen Engineer]]></dc:creator><pubDate>Sat, 27 Jun 2026 15:22:29 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/64479ed989b09f5069ab7a18/4e5ba956-e990-4afb-ab8f-fe44876ed120.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I usually go out and join tech events in the city. I’ve joined several of them, but the most memorable was the event where my friends and I won first prize by solving a delivery optimization problem. The event was hosted by a delivery platform named Honestbee Logistics, which at the time was exploring the possibility of opening a new office in Saigon, Vietnam.</p>
<p>As I remember, the problem was about finding the fastest route to deliver goods, considering a few factors such as the distance between addresses, the weight of the goods, and the value of the goods. We assigned a score in several dimensions, from high to low:</p>
<ul>
<li><p>Distance: nearer deliveries get higher scores</p>
</li>
<li><p>Weight: lighter packages get higher scores</p>
</li>
<li><p>Value: higher-value goods get higher scores</p>
</li>
</ul>
<p>I don’t remember the exact formula, but we computed a score for each data point to decide which location to visit next in the route.</p>
<p>For the algorithm, we used a simple BFS-based approach on top of this scoring. It wasn't a perfect solution to the full vehicle routing problem, but under hackathon time pressure, a clear heuristic plus a straightforward search was good enough to find fast routes and keep us competitive on the leaderboard.</p>
<p>This was in 2016, when AI was nowhere to be found in our toolbox. We coded everything by hand, with no tests, and had to make it runnable within a few hours and submit it to an API that powered a real-time leaderboard. It felt like a game to us, watching the leaderboard update with our names and our rivals’. In the end, the top 10 submissions were all ours. We successfully delivered all the goods using the fastest routes.</p>
<p>We won the first prize. The challenge was fun, and my teammate and I had a great bonding experience. We weren’t the smartest individuals, but together we achieved something much bigger than what either of us could have done alone.</p>
<p>I learned that day that when you create a good environment where people feel challenged but also safe and have fun doing their work, they will perform at their best.</p>
<p>Here was the certificate</p>
<img src="https://cdn.hashnode.com/uploads/covers/64479ed989b09f5069ab7a18/0b67b34f-3158-476d-b4d9-d87cf299c75a.jpg" alt="" style="display:block;margin:0 auto" />

<p>On the leaderboard, the higher at the bottom was better.</p>
<img src="https://cdn.hashnode.com/uploads/covers/64479ed989b09f5069ab7a18/16bb1e5a-2e40-4a6a-91d8-877b099d6ef7.jpg" alt="" style="display:block;margin:0 auto" />

<table>
<thead>
<tr>
<th>Team</th>
<th>Submission Time (estimated)</th>
<th>Score</th>
</tr>
</thead>
<tbody><tr>
<td><code>toanduong/ToanDuong</code></td>
<td>Sat Nov 26 2016 14:07:12 GMT+0700 (ICT)</td>
<td>0</td>
</tr>
<tr>
<td><code>long.polygioi/LongPhatVIP</code></td>
<td>Sat Nov 26 2016 14:09:48 GMT+0700 (ICT)</td>
<td>0</td>
</tr>
<tr>
<td><code>anhtu/bluebird</code></td>
<td>Sat Nov 26 2016 14:11:36 GMT+0700 (ICT)</td>
<td>52,184</td>
</tr>
<tr>
<td><code>anhtu/bluebird</code></td>
<td>Sat Nov 26 2016 14:13:51 GMT+0700 (ICT)</td>
<td>52,184</td>
</tr>
<tr>
<td><code>anhtu/bluebird</code></td>
<td>Sat Nov 26 2016 14:16:27 GMT+0700 (ICT)</td>
<td>52,184</td>
</tr>
<tr>
<td><code>honestbe/vrp_sample</code></td>
<td>Sat Nov 26 2016 14:27:59 GMT+0700 (ICT)</td>
<td>122,475</td>
</tr>
<tr>
<td><code>vhit/vrp_challenge</code></td>
<td>Sat Nov 26 2016 14:43:18 GMT+0700 (ICT)</td>
<td>169,342</td>
</tr>
<tr>
<td><code>vhit/vrp_challenge</code></td>
<td>Sat Nov 26 2016 14:56:41 GMT+0700 (ICT)</td>
<td>169,342</td>
</tr>
<tr>
<td><code>vhit/vrp_challenge</code></td>
<td>Sat Nov 26 2016 15:10:05 GMT+0700 (ICT)</td>
<td>169,342</td>
</tr>
<tr>
<td><code>vhit/vrp_challenge</code></td>
<td>Sat Nov 26 2016 15:31:42 GMT+0700 (ICT)</td>
<td>169,342</td>
</tr>
<tr>
<td><code>vhit/vrp_challenge</code></td>
<td>Sat Nov 26 2016 16:02:49 GMT+0700 (ICT)</td>
<td><strong>171,140</strong></td>
</tr>
</tbody></table>
<p><code>vhit</code> was the name of our school, We named the team <code>vhit</code> to honor our school of <strong>Vietnam - Korea University of Information and Communication Technology</strong> that we all graduated in the same year of 2012.</p>
<p>I only had this blurry picture of the band, the guy on the left is <a href="https://www.linkedin.com/in/minh-tu%E1%BA%A5n-hu%E1%BB%B3nh-2138b0a1/">Huỳnh Minh Tuấn</a>, the guy on the right is <a href="https://www.linkedin.com/in/hank-ph%C3%B9ng-%C4%91%C3%B4ng-4791b857/">Phùng Đông Hưng</a>.</p>
<img src="https://cdn.hashnode.com/uploads/covers/64479ed989b09f5069ab7a18/dfc2b5c7-6d16-4115-93f8-183786582ddf.jpg" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/64479ed989b09f5069ab7a18/69843ebc-c05f-4b05-97b7-68a364060f19.jpg" alt="" style="display:block;margin:0 auto" />]]></content:encoded></item><item><title><![CDATA[Migrating a decade of production data from an abandoned database (RethinkDB) to MongoDB]]></title><description><![CDATA[Back in 2015, when the Node.js, NoSQL, and microservices ecosystem was still fresh and the tooling hadn’t matured, choosing one solution over the other would be a hit-and-miss situation. We didn’t kno]]></description><link>https://nguyengineer.dev/migrating-a-decade-of-production-data-from-an-abandoned-database-rethinkdb-to-mongodb</link><guid isPermaLink="true">https://nguyengineer.dev/migrating-a-decade-of-production-data-from-an-abandoned-database-rethinkdb-to-mongodb</guid><category><![CDATA[RethinkDB]]></category><category><![CDATA[MongoDB]]></category><category><![CDATA[Databases]]></category><category><![CDATA[migration]]></category><category><![CDATA[Go Language]]></category><dc:creator><![CDATA[Nguyen Engineer]]></dc:creator><pubDate>Thu, 25 Jun 2026 11:08:35 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/64479ed989b09f5069ab7a18/31015fb9-a990-4359-8be3-8f2769f6df76.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Back in 2015, when the Node.js, NoSQL, and microservices ecosystem was still fresh and the tooling hadn’t matured, choosing one solution over the other would be a hit-and-miss situation. We didn’t know if the technologies we chose would live along with the product we built on them. Luckily, some of the choices were right, like we chose React over AngularJS. Google quickly abandoned AngularJS 1.0’s architecture, and the version 2.0 is a totally different architecture. But we weren’t lucky in choosing RethinkDB over MongoDB. In 2015, they were still competitors, and RethinkDB was better for several reasons:</p>
<ul>
<li><p>Realtime by default: RethinkDB gave you native changefeeds, so apps could subscribe to live query updates instead of building polling or oplog-tail logic around MongoDB.</p>
</li>
<li><p>Better fit for live apps: It was designed for dashboards, collaborative tools, multiplayer systems, and other “realtime web” use cases where MongoDB was more of a general document store.</p>
</li>
<li><p>ReQL was more elegant: RethinkDB’s query language was chainable and embedded in your host language, so it felt cleaner than assembling MongoDB-style query documents and driver calls.</p>
</li>
<li><p>More powerful querying: RethinkDB emphasized joins, subqueries, and aggregations in the database, which made complex document queries easier than MongoDB’s then-limited relational story.</p>
</li>
<li><p>Simpler clustering: Sharding and replication were presented as easy to set up, with a web admin UI that reduced operational friction.</p>
</li>
<li><p>Stronger consistency story: RethinkDB marketed itself as “a MongoDB done right,” stressing MVCC, non-blocking writes, durability by default, and better correctness trade-offs.</p>
</li>
<li><p>More attractive developer experience: Reviews from the time repeatedly called out ease of use, clear docs, and “just works” realtime behavior as reasons people got excited about it.</p>
</li>
<li><p>MongoDB needed extra plumbing: If you wanted similar realtime behavior in MongoDB, you usually had to add another layer yourself, which is why RethinkDB felt more complete for that niche.</p>
</li>
</ul>
<p>Internally, the product team also aimed to build a real-time web app. We chose React to build the SPA frontend. And as the team said, the backend also using a real-time database “makes perfect sense".</p>
<p>Then on a rainy day out of the blue, RethinkDB company announced that they will shut down <a href="https://rethinkdb.com/blog/rethinkdb-shutdown/">https://rethinkdb.com/blog/rethinkdb-shutdown/</a>. No more new features or bug fixes. They left us with one of the biggest technical debts. Migrating away from it is obvious, but one of the company's most competent features is built on top of it. The company is also a startup, so releasing new features to get to the top of the market is the most important thing. And the plan to migrate from it was delayed for years, until the day the product is mature, and the coming of AI makes it feasible.</p>
<p>In this post I’ll walk through why none of the obvious migration approaches fell apart under our constraints, how we designed a Data API to hide the migration from the rest of the team, and how a purpose-built Go migrator with distributed locks and time-windowed checkpoints moved terabytes of live data with only minutes of downtime.</p>
<h2>Problem statements</h2>
<p>Migrate away from RethinkDB to MongoDB is one of our multi-year plans to modernize the system. With over TB of data, 200+ tables, and the 99.9% downtime requirement, we need a solid migration process and tool that can tolerate errors and run the migration simultaneously on several tables, while maintaining data consistency.</p>
<p>RethinkDB is no longer maintained, so the modern approach like using the CDC doesn’t exist. The nearest thing to CDC is the native RethinkDB change feeds, which is unreliable because the change feeds only work if the consumer is live. If the network blips or the migrator is restarted, then we lose the data changes.</p>
<p>As we migrate data, we still need to serve the production traffic, an event-driven system serving 30M+ events per day, we can only put the system to maintenance mode for like 10 minutes during the night.</p>
<p>Some of the proposed solutions are:</p>
<ul>
<li><p>Dump the data from RethinkDB then import to MongoDB: This doesn't work because the dump requires a long downtime, and the dumping tools for RethinkDB are also slow and unmaintained, the MongoDB import tool is also slow on hundreds of GBs of data, and if there are failures we need to start from the beginning.</p>
</li>
<li><p>Stream the data directly: this also doesn't work because it can't guarantee the data consistency on live system. Stream data is a point-in-time cursor scan, any document inserted, updated, or deleted after the cursor has already passed it will be silently missed.</p>
</li>
<li><p>Use RethinkDB change feeds: As I mention above, it is unreliable and the event doesn't guarantee to delivery at least once, if the connection drops, all events that occurred during the gap are permanently lost with no built-in offset or replay mechanism to resume from.</p>
</li>
</ul>
<p>The 10+ years code base is also a challenge when the RethinkDB queries embedded into both the test and code files across 3 languages Nodejs, Go, and C# within million lines of code.</p>
<p>And most importantly, we are not only migrating data in a live production system, we also migrate the code where the rest of the team is still pushing the product feature every day. We can't just block the deployment until we finish the code; we need to merge the code gradually and give the team the room to hot fix their product feature issue if any.</p>
<p>I will break down the problems and solutions, considering the trade-offs involved. Every problem has sub-problems, and so do the solutions. We have made several important decisions, and I hope to have enough time and space to present them all. However, if anything is unclear, feel free to leave a comment. I will reply to it or update the post to address it.</p>
<h3>The general solution</h3>
<img src="https://cdn.hashnode.com/uploads/covers/64479ed989b09f5069ab7a18/c82c4154-e57f-4512-886b-5800daf342df.png" alt="" style="display:block;margin:0 auto" />

<p><mark class="bg-yellow-200 dark:bg-yellow-500/30">The key requirement is how to migrate the data while the team still work on the same code base, build feature that requires database query while the migration code and data change keep moving spontaneously</mark></p>
<p>We considered two approaches</p>
<ol>
<li><strong>Approach A: Direct query replacement behind a</strong> <code>migrationStatus</code> <strong>flag</strong></li>
</ol>
<pre><code class="language-typescript">// Run a database operation depending on current migration status
procedure runQuery(oldDbOperation, newDbOperation):

  if migrationStatus is NOT_STARTED:
    // operate only on old database
    result = execute oldDbOperation
    return result

  else if migrationStatus is MIGRATING:
    // operate on both databases, in parallel
    resultFromOld = execute oldDbOperation
    execute newDbOperation
    wait until both operations are finished
    // use old database result as the source of truth during migration
    return resultFromOld

  else if migrationStatus is MIGRATED:
    // operate only on new database
    result = execute newDbOperation
    return result

  else:
    // invalid state
    signal error "Invalid migration status"
</code></pre>
<ol>
<li><strong>Approach B: Data API over HTTP</strong>, like the Supabase API, where all database access goes through a shared HTTP service instead of a direct SDK. So instead of a query like <code>SELECT name FROM users WHERE userId = x</code>, callers just call <code>await readDocumentById('users', id, ['name'])</code></li>
</ol>
<p>In the scope of this post, I will only introduce the Data API pattern but not dig deep into it, it is a sophisticated service and worth a separate post to cover the details. What is relevant here is that each table has a <code>migrationStatus</code> field in the migrator's tracking document, and the Data API reads it on every request to decide which database to talk to. During <code>Migrating</code>, every write goes to both databases and reads come from RethinkDB. Once an operator marks a table <code>Migrated</code>, the Data API drops RethinkDB entirely for that table and routes all traffic to MongoDB — with no deployment and no code change required.</p>
<p>We chose the Data API approach after considered several trade offs</p>
<ul>
<li><p>Accessibility over the performance. Other team also need to access the same database, if we choose <strong>(1)</strong>, the other team need to write the new query and the migration themselves. The Data API will expose as the HTTP server so other team can just call the Data API without knowing which database serving the request.</p>
<ul>
<li>We also need the Data API to auto-scale based on workload. Ideally, we will keep it inside a Kubernetes cluster, but Cloud Run offers the same horizontal and vertical scaling ability and the ease of access to other team members. Then, we choose Cloud Run for deployment.</li>
</ul>
</li>
<li><p>Fast execution over performance. If we use the migrator in approach <strong>(1),</strong> we need to replicate this across 3 implementations in Node.js, C#, and Go, not to mention the direct RethinkDB query in the test files across 3 languages. By replacing it all by HTTP calls with the unified interface, we hide all the migration behind. The team can continue to work on the feature without knowing the migration behind the Data API call. If we just replace old queries by the new queries, it costs us the same effort and slows down the execution as the team still works on the same codebase; conflict is inevitable.</p>
</li>
</ul>
<p>The system is mature, so we need to design the solution based on the existing infrastructure and design rules:</p>
<ul>
<li><p>The services and workers running inside the Kubernetes cluster for auto-scaling.</p>
</li>
<li><p>The workers can't access the database directly; only the service can call the database to prevent the exhausted connection pool by having too many worker instances.</p>
</li>
<li><p>The database is outside of the Kubernetes cluster to provide easy access to other teams as we shared the database. Ideally, each team will own their database, but in practice, to save costs, we usually use the same database instance.</p>
</li>
</ul>
<h2>The database migration tool</h2>
<p>Here is the main part that keeps the migration process safe and fast. But first, let's review the requirements again so I can explain the design decisions to satisfy them.</p>
<ul>
<li><p>TB of data is live on production while all the migrations run in the background. We can only tolerate a few minutes down at midnight.</p>
</li>
<li><p>Data integrity is non-negotiable; rollback is hard because the live data keeps writing to the database.</p>
</li>
<li><p>Avoid the operational risk of human mistakes.</p>
</li>
<li><p>The timeline is tight; we need to migrate tables in parallel to speed it up.</p>
</li>
<li><p>We need some data manipulation before inserting data from RethinkDB to MongoDB because the two systems handle absent values differently. RethinkDB stores explicit <code>null</code> fields inside documents, while MongoDB applications typically treat a missing field and null as distinct things. Keeping those null fields would silently change query behavior, so the migrator recursively strips all null values from every document — including nested objects and arrays — before writing to MongoDB.</p>
</li>
<li><p>The process should be resilient, tolerant of network issues, server crashes, and running inside Kubernetes to reuse the existing infrastructure and vertically auto-scale.</p>
</li>
</ul>
<p>I came up with the design, in general.</p>
<p>The migration tool is a single Go service: a web dashboard to control migrations and a background worker engine. One binary, deployed in Kubernetes.</p>
<img src="https://cdn.hashnode.com/uploads/covers/64479ed989b09f5069ab7a18/62c63296-d3e4-4487-b94c-d1f8ac417830.png" alt="" style="display:block;margin:0 auto" />

<h3>Why Go</h3>
<p>The migrator needs to run up to at least 20 concurrent workers, sustain steady memory pressure for days at a time, and stay maintainable by a single engineer who may not touch it for months. Node.js was ruled out early — it's hard to hold a long-running process steady under high heap load, and its event loop model makes true parallelism awkward when you need goroutine-per-worker isolation with clean, explicit cancellation. Go's goroutines and <code>sync</code> primitives gave us that naturally, and the <code>context</code> package made cancellation and timeout propagation explicit all the way down the call stack — critical when a crashing pod must release a distributed lock cleanly.</p>
<p>Static typing also mattered. A migration tool is not product code; no one is going to refactor it frequently, and the person who picks it up next might not have full context. Go's compiler catches whole classes of mistakes before they reach production, and Go's compatibility guarantee means the binary compiled today will still build and run on a future toolchain without surprises — no npm audit roulette, no breaking major version upgrades to chase.</p>
<p>Building this tool also sharpened my Go fundamentals in ways I hadn't expected: designing worker pools that are free of race conditions, using <code>sync</code>, <code>context</code>, and <code>log/slog</code> idiomatically instead of pulling in frameworks, and keeping the dependency count low so the tool can outlast the migration itself without rotting.</p>
<p>The dashboard was a deliberate choice too. Instead of a React frontend, a separate API, and a build pipeline to maintain, I used Go's <code>html/template</code> package and wired interactions with <a href="https://htmx.org/">htmx</a>. Every action — start, stop, reset, reclaim a lock — is a single HTML attribute and one server endpoint. No JavaScript, no bundler, no frontend dependencies to audit. Combined with Go's <code>embed</code> package for static files and the table config CSV, the entire service — migrator engine, scheduler, and dashboard — ships as one binary. Deploying an update in Kubernetes is rolling out one image.</p>
<p>The table list is an embedded CSV in the binary — table names, collection names, connection strings. On startup the service seeds a tracking document per table into MongoDB. This is the source of truth: migration phase, progress, and lock state for each table. It also computes a safe batch size per table from RethinkDB stats (<code>100MB ÷ avgDocSize</code>) to keep memory usage bounded.</p>
<h3>Scheduling and distributed lock</h3>
<p>Every 30 seconds, <code>BootstrapWorker</code> loads all <code>Migrating</code> tables, sorts by who ran least recently, and fills up to 20 worker slots. When it wants to start a worker, it does one atomic <code>FindOneAndUpdate</code> in MongoDB to claim the lock. If another pod holds a valid lock, it skips that table. Locks have a 3-minute TTL with a 1-minute heartbeat — if a pod crashes, the lock expires and another pod picks it up.</p>
<pre><code class="language-markdown">BootstrapWorker (every 30s)
  ├── worker already running?  ──YES──► skip
  ├── 20 slots full?           ──YES──► wait next tick
  ├── lock acquired?           ──NO───► skip (other pod)
  └──► Start Worker
</code></pre>
<p>Workers run one cycle then exit, freeing their slot. The 30-second tick is the loop — this is what lets the scheduler fairly rotate across all 150 tables instead of 20 workers running forever and starving the rest.</p>
<h3>What one worker cycle does</h3>
<img src="https://cdn.hashnode.com/uploads/covers/64479ed989b09f5069ab7a18/d5816457-014f-4f49-8c48-1224773034d8.png" alt="" style="display:block;margin:0 auto" />

<p>The write hook in step 1 is the backbone of the whole sync: RethinkDB automatically stamps <code>__lastModifiedAt</code> on every insert or update, which is what lets the data sync query only what changed since the last cycle.</p>
<p>Each worker runs three steps in sequence. First, it verifies the <code>__lastModifiedAt</code> index exists on the RethinkDB table and creates it if missing. Then it processes any pending deletions that the Data API has already recorded in MongoDB. Finally, it runs a <code>Between</code> range scan on the <code>__lastModifiedAt</code> index for the bounded time window, reads documents in batches, strips null fields from each one, and bulk-upserts them into MongoDB using <code>ReplaceOne</code> with <code>upsert: true</code>, keyed on the table's RethinkDB primary key. The <code>lastBackfillAt</code> checkpoint is written to the tracking document only after the full time window commits successfully — so a crash means at most one 15-minute window is re-processed on the next cycle, not the entire table.</p>
<p>For data sync, the time window advances each cycle with a small backward overlap to handle <strong>clock skew</strong> and data <strong>replication lag</strong>, and a 15-minute forward cap so a large backlog gets drained incrementally rather than in one huge scan.</p>
<pre><code class="language-plaintext">──────────────────────────────────► time
[   previous cycle   ][─5min][  this cycle ≤15min ]
                             ▲                     ▲
                          fromDate                toDate
                                  (lastBackfillAt moves here)
</code></pre>
<h3>Here is a few of the most asked questions about this tool</h3>
<p><strong>Q: Old documents in RethinkDB never had</strong> <code>__lastModifiedAt</code> <strong>— how does the first sync pick them up?</strong></p>
<p>A: The index creation defaults missing <code>__lastModifiedAt</code> to now at index build time, grouping all old docs under the same timestamp. The first run also has no cap — it scans from the beginning of time to now, so everything gets swept in one pass.</p>
<p><strong>Q: Who writes the deletion tracking records — the migrator?</strong></p>
<p>A: No. The Node.js Data API writes a tracking record into MongoDB before and after every delete from RethinkDB. The migrator only reads that collection. It never observes RethinkDB deletes directly.</p>
<p><strong>Q: What if the migrator crashes mid-table?</strong></p>
<p>A: The lock expires in ≤3 minutes. The next BootstrapWorker tick on any surviving pod claims it and resumes from <code>lastBackfillAt</code> — the last committed checkpoint. At most 15 minutes of work is repeated due to the cycle cap.</p>
<p><strong>Q: What if a document is updated in RethinkDB while the migrator is currently processing that same window?</strong></p>
<p>A: The write hook updates its <code>__lastModifiedAt</code> to now, pushing it past toDate. The next cycle's window picks it up. The 5-minute backward overlap also acts as a safety buffer.</p>
<p><strong>Q: How do you know when a table is fully migrated and safe to cut over?</strong></p>
<p>A: The dashboard shows migratedDocCount vs totalRethinkDocs. When they converge and deletions are drained, the table is a candidate for cutover. The actual switch happens at midnight: we put the system in maintenance mode for a few minutes, deploy the feature flag values that tell the Data API to read from MongoDB for the target tables, and during those minutes the data is identical in both databases so there is no risk window. Then we click Complete in the dashboard to stop the migration workers for those tables and bring production back up. After that we run regression and smoke tests on prod to confirm. We also have a rollback plan if anything looks wrong, but that deserves its own post.</p>
<p><strong>Q: 20 parallel workers still means 20 simultaneous reads on RethinkDB — is that fine?</strong></p>
<p>A: Each worker reads at most ~100MB per batch, not the full table. The 20-slot cap was tuned to the cluster's comfortable read concurrency. It's light, sustained pressure rather than a spike. And we can always increase this limit.</p>
<p><strong>Q: How do you verify the migrated data is identical between RethinkDB and MongoDB?</strong></p>
<p>A: We built a separate validation worker. After every successful read through the Data API, the service asynchronously publishes a verification message to a Pub/Sub topic — fire-and-forget, so it never adds latency to the actual request. A 10-minute deduplication window prevents the same read from flooding the queue.</p>
<p>The worker picks up the message, replays the identical request against both the RethinkDB and MongoDB endpoints concurrently, then normalizes both responses: stripping database metadata fields (<code>_id</code>, <code>__lastModifiedAt</code>), removing nulls, and sorting keys alphabetically for a stable comparison. Then it runs a deep equality check. On any mismatch it generates a detailed diff and logs it.</p>
<p>This only runs on tables still in the <code>Migrating</code> phase, so real production traffic continuously validates the migration without any overhead on already-completed tables.</p>
<h2>Results</h2>
<p>So far, 100+ tables and over a TB of data have been fully migrated without incident. That doesn't mean the path was smooth — we hit OOM panics in the early weeks and found some bugs along the way. Both were addressed: OOM by tightening the batch size calculation, bugs by patching the worker logic. The service has 95% test coverage across unit and integration tests, and we ran it on staging for months before touching production, so most of the hard surprises were already behind us by the time real data was involved.</p>
<p>Throughput isn't our bottleneck — the migration runs in parallel with the team rewriting direct RethinkDB queries to Data API calls, so we don't rush it. That said, the tool can move roughly a TB within a single working day when given the bandwidth. And because the pull-and-lock model is stateless, scaling is just adding pods — no coordination required.</p>
<p>Observability is deliberately simple: per-table progress on the dashboard, worker errors logged directly into the tracking document, and Kubernetes resource metrics for the rest. We haven't wired up external alerting yet — it wasn't worth the integration cost for an internal tool at this stage — and the dashboard has been enough to catch issues in practice.</p>
<h2>Final thoughts</h2>
<p>One thing that isn’t obvious from the architecture: before this tool existed, migrating a table meant running bash scripts manually — on a dev machine, a VM, or a Cloud Run instance — each one adjusted by hand per table. Miss a step or run them out of order and the table ends up in an unknown state with no easy recovery. With 200+ tables in the pipeline, that was a real operational risk, not a hypothetical.</p>
<p>The tool replaced all of that with a few clicks on a dashboard. What used to take hours or a full day for a single table now takes minutes. And the company stopped carrying a category of risk it didn’t even know it had.</p>
<p>In the end, this migration wasn’t about some clever trick. It was about surviving a decade of history sitting in an abandoned database, while a 30M+ events/day system kept running and the rest of the team kept shipping. Almost every “simple” idea fell apart once we accounted for downtime limits, consistency, and human error. What worked was a combination of boring, reliable pieces: a Data API in front of the databases, a purpose-built Go migrator with locks and checkpoints, small bounded windows, and a process designed so that failure just meant “retry later,” not “panic at midnight.”</p>
<p>We’re still migrating. But months in, the tool has held up, the team keeps shipping without noticing, and we haven’t had a data incident. For a years-overdue migration running in the background of a live system, that’s good enough.</p>
]]></content:encoded></item><item><title><![CDATA[Developers, what should we do next in the age of AI?]]></title><description><![CDATA[At this point, April 2026, everyone is familiar with AI integrated deeply into the software development process. Some worry that AI will eventually replace or devalue them. I know that's the common co]]></description><link>https://nguyengineer.dev/developers-what-should-we-do-next-in-the-age-of-ai</link><guid isPermaLink="true">https://nguyengineer.dev/developers-what-should-we-do-next-in-the-age-of-ai</guid><category><![CDATA[TDD (Test-driven development)]]></category><category><![CDATA[AI]]></category><category><![CDATA[Product Engineering]]></category><category><![CDATA[Career]]></category><category><![CDATA[SRE]]></category><dc:creator><![CDATA[Nguyen Engineer]]></dc:creator><pubDate>Sat, 11 Apr 2026 19:46:07 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/64479ed989b09f5069ab7a18/c3d07aeb-2c10-46a9-aea8-e78c7b9ceee3.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>At this point, April 2026, everyone is familiar with AI integrated deeply into the software development process. Some worry that AI will eventually replace or devalue them. I know that's the common concern, I don't need and don’t want to brag about it. There are some practices I believe developers should do next.</p>
<h2>Strengthen the tech stack and domain expertise</h2>
<p>"AI can write all the code, why bother choosing the language?" You may have heard this already, but the answer is simple: a language is tied to its ecosystem, it takes years to become an expert in a tech stack, and we rely on that expertise to judge the solutions provided by AI.</p>
<p>The code looks fine, and all the tests pass, but is it safe to deploy? Not quite. AI makes several architecture decisions no one is aware of, like using long expiration days for the auth token in a fintech product, choosing a low-maintenance npm package from some unknown developer, or heavily investing time in edge cases that never exist in real life.</p>
<h3>Taste and judgement</h3>
<p>So, being fundamentally strong in your company's tech stack is even more valuable in the AI age. AI can write code, but we still need to review and sometimes rewrite it, and redirect AI toward better code. We are opinionated here, not just accepting any code. Our taste is proven and solid, let it dictate the way new code is written. You're familiar with the system you own, your team's coding conventions, your battle scars. Even though all conventions are written into some instructions, AI can still go off the rails sometimes.</p>
<p>Regarding domain knowledge, AI can't replace anyone outright yet. Even in a scenario of a many-years complex project system, even though we can implement new features or fix bugs tangled in layers of complexity, anyone on the team can just drop in Opus 4.6 and a few lines of requirements and AI can implement a lot of that, it's still not the end of your expertise. Companies still need people to drive development. The metrics below show exactly where that human edge lives.</p>
<h3>Drive the output via TDD</h3>
<p>This is real-life experience from the team I'm working in, and several others have experienced the same: nothing drives AI output better than the TDD approach. You can also think of spec-driven development in the same mindset, we must understand what we need to do first and force the AI to actually understand what it's going to build.</p>
<blockquote>
<p>When you're directing thousands of lines of code generation, you need a forcing function that makes you actually understand what's being built. Tests are that forcing function. -- Martin Fowler</p>
</blockquote>
<p>We don't even need to write the tests line by line. Given the existing tests in the system, we can instruct AI to build new tests based on existing patterns and feed it our requirements for the expected output. It needs to write the test first and run it, the test should fail, then write the code and run the test again, keeping fixing the code in a loop until all green. With AI, TDD is easier than ever.</p>
<h3>Context Engineering</h3>
<p>AI providers keep improving models to handle larger and larger context windows, but that doesn't mean we can put the whole codebase into one prompt. Models don't know where to focus their attention when the provided context is too large. At enterprises, codebases can be millions of lines of code, not to mention that logs and real production data can be large in some cases.</p>
<p>Also, code is not everything; logs, metrics, ticket systems, and human discussions are inputs as well. We need to carefully compose good context for a task: that can mean referencing the relevant ticket, including meeting notes on the solution, querying related data and logs, and embedding them into the prompt. This process is often called context engineering; in an agentic setup, it becomes one part of a broader “harness engineering” discipline that also covers tools, repositories, and feedback loops.</p>
<h3>Observability and Debugging</h3>
<p>I mapped my debugging skills into an agent and shared it with the team. In general, it's the skill of looking for related data in logs, metrics, and traces. In detail, it includes grabbing traceable information like traceId and entityId, finding the related logs to query in our Elasticsearch database, then locating the relevant code that produces those logs, then asking for a real piece of data, looking for related tickets and discussions around the issue. That is my expertise, collected over years of working on the system, that's not something AI can easily establish without a human to orchestrate it. Not to mention production access: we can't give it to AI directly. Who knows what a hallucinating AI agent might do.</p>
<p>As a result, this AI agent alone reduced my MTTR by 12x, from 1 hour to 5 minutes. In most cases, within 5 minutes I can identify what is wrong with the incident we're facing, compared to hours of investigation before the AI age.</p>
<p>But if I stop feeding my skills to AI, stop providing the correct data through my authority, and stop judging the AI's investigation results, the output becomes meaningless. As I keep improving, the AI also improves through my discoveries.</p>
<h2>Metrics</h2>
<p>Technical expertise sets the foundation, but metrics are how we keep score. Here's where the human edge becomes measurable.</p>
<h3>Coding metrics</h3>
<p>Thanks to AI, we can now produce more options and more solutions to the same problem. For code, there are some simple metrics we can measure right after solutions are provided, like Cognitive Complexity.</p>
<blockquote>
<p>Cognitive Complexity specifically measures how difficult code is for humans to understand, heavily penalizing nested control structures like if statements, loops, and switches.</p>
</blockquote>
<p>We can reinforce it through instructions, but sometimes I still need to edit the code manually to maintain readability. Other metrics we can use are Cyclomatic Complexity, Maximum Nesting Level, and Duplicated Lines (%).</p>
<h3>Product metrics</h3>
<p>As we, developers, step into higher-value work, the product-centric metrics are what we need to be aware of and contribute to after the coding step. Metrics like feature adoption, clicks, retention, conversion, and search matching rate are usually the KPIs of the product team, but understanding them will bring developers closer to the customer. It's what companies usually dream of: a product engineer.</p>
<blockquote>
<p>A product engineer (or product-minded engineer) blends software engineering with product thinking. They code features while considering user needs, business impact, and data, beyond pure tech tasks.</p>
</blockquote>
<h3>Service Level Metrics</h3>
<p>Service Level Metrics are the most important for developers. They're tied into the contract we signed with customers, so they must be satisfied, that's a direct impact on the company. Example:</p>
<table>
<thead>
<tr>
<th>#</th>
<th>Service</th>
<th>SLI (Measured)</th>
<th>SLO (Target)</th>
<th>SLA (Promise)</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>Web Latency</td>
<td>p95=200ms</td>
<td>&lt;300ms 99%</td>
<td>&lt;500ms 99%</td>
</tr>
<tr>
<td>2</td>
<td>Login Time</td>
<td>Avg=1.5s</td>
<td>&lt;2s 95%</td>
<td>&lt;3s 99%</td>
</tr>
<tr>
<td>3</td>
<td>Checkout</td>
<td>99.2% success</td>
<td>&gt;=99.95%/7d</td>
<td>&gt;=99.9%</td>
</tr>
<tr>
<td>4</td>
<td>Email Delivery</td>
<td>97% inboxed</td>
<td>&gt;=99%/day</td>
<td>&gt;=98% or refund</td>
</tr>
<tr>
<td>5</td>
<td>Support Tickets</td>
<td>Avg=30min</td>
<td>&lt;45min 95%</td>
<td>&lt;1hr response</td>
</tr>
<tr>
<td>6</td>
<td>Cache Hit</td>
<td>85% hits</td>
<td>&gt;=90%/hour</td>
<td>&gt;=85%</td>
</tr>
<tr>
<td>7</td>
<td>Uptime</td>
<td>99.92%</td>
<td>&gt;=99.95%/mo</td>
<td>&gt;=99.9% or refund</td>
</tr>
</tbody></table>
<p>Take this example from the internet to understand it better:</p>
<blockquote>
<p>During a traffic spike: SLI drops to 99.7% (outages from nested if bugs in your API). SLO breached → investigate, error budget exhausted → prioritize fixes over features. SLA holds (still &gt;99.9%), avoiding penalties. Ties back to code reviews: Low Cognitive Complexity prevents such failures.</p>
</blockquote>
<p>Metrics lead all the way back to decision-making when prompting, reviewing, and refining code to ensure business value. We haven't given AI this level of thinking yet, and AI also doesn't go to a client and sign the SLA contract, doesn't take charge of the penalties. Only humans are accountable for it. It leaves us with questions like:</p>
<ul>
<li><p>Should we implement this feature at all? What is the benefit?</p>
</li>
<li><p>Among 3 proposed solutions, what technical debt can we tolerate?</p>
</li>
<li><p>The solution is clear, but it requires 48 hours of work for a low-value ticket, is it worth it?</p>
</li>
<li><p>Our biggest client can produce 30M events at peak, what do we need to do to ensure the SLA?</p>
</li>
</ul>
<h2>Ownership and Accountability Mindset</h2>
<p>AI can't be assigned to PagerDuty and join an incident call at 3:00 AM. It can help in the incident investigation process, but do customers want to show up on a call with an AI agent during an incident? Definitely not. When SLAs are breached and penalties land, that weight falls on a human, and always will.</p>
<p>Based on the foundation of technical expertise and metrics-centric development above, we can see that the more AI absorbs the doing, the more valuable the person who takes responsibility becomes.</p>
<h2>Communication and Collaboration</h2>
<p>One of my favorite communication skills is translating the technical focused discussions into language that product people can understand, while keeping it relevant and precise. People love the summarize button, but who can ensure that summary is still correct? Only the person sitting between these layers can take responsibility for it.</p>
<p>At this point, I think we can all agree that we can't just prompt all the way to production, and on the other side, engineers can't only work on code when AI can do that part most efficiently. Pure software engineers are elevating to product engineers, who can orchestrate the technical components to satisfy requirements from the product and the customer, and can also communicate well between stakeholders.</p>
<img src="https://static.wikia.nocookie.net/matrix/images/f/f3/Architect_%26_neo.png/revision/latest" alt="Neo and the Architect" style="display:block;margin:0 auto" />

<p><em>I remember the scene from The Matrix Reloaded where Neo tells the Architect that the machines need human beings to survive. Even though AI has assimilated most human knowledge, without us, it would stagnate, waiting for new human discoveries. We are not becoming obsolete. We are becoming the ones who decide what gets built, why it matters, and who answers for it.</em></p>
]]></content:encoded></item><item><title><![CDATA[Start With a Monolith. Split It Only When You Have To]]></title><description><![CDATA[Conway's Law says systems end up mirroring the communication structure of the teams that build them. A small team produces a unified system; a distributed organization produces distributed software. W]]></description><link>https://nguyengineer.dev/why-i-start-with-a-monolith</link><guid isPermaLink="true">https://nguyengineer.dev/why-i-start-with-a-monolith</guid><category><![CDATA[Microservices]]></category><category><![CDATA[monolith]]></category><category><![CDATA[architecture]]></category><category><![CDATA[Go Language]]></category><dc:creator><![CDATA[Nguyen Engineer]]></dc:creator><pubDate>Fri, 30 May 2025 08:11:46 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1748592635254/c117adff-497b-4f47-9417-c620345b6cc0.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Conway's Law says systems end up mirroring the communication structure of the teams that build them. A small team produces a unified system; a distributed organization produces distributed software. When I see a five-person team running 60 microservices, I see a team fighting that law — and losing.</p>
<p>That observation shapes every architectural decision I make on a new project. The shape of my system should follow the shape of my team, not the other way around.</p>
<p>Most teams over-engineer day one and pay for it for years. I've worked on projects that turned into maintenance nightmares — some eventually rescued, but only after burning serious time and money. The technical debt followed them long after. It doesn't have to go that way.</p>
<p>When I'm building something new with a small team, I focus on functionality, not scale. A vertically scaled, stateless server behind a good proxy like Traefik gets me much further than I once thought possible. Here's the architecture I reach for today.</p>
<h2>My Majestic Monolith</h2>
<p>Five principles I treat as non-negotiable:</p>
<p><strong>1. Clean architecture from day one.</strong> I keep my business logic independent of my infrastructure. It's just separation of concerns, but the payoff compounds. When I eventually need to extract a service — when team structure or workload shape demands it — only the infrastructure layer changes. The core logic runs the same whether it lives in a VM, a Kubernetes pod, a Lambda, or a Cloud Function.</p>
<p><strong>2. One capable server.</strong> I pick a runtime that can fully saturate the hardware I'm paying for and run uninterrupted for months. For a long-lived monolith, I look for efficient use of multiple cores under sustained load, predictable memory behavior over weeks of uptime, and a mature concurrency model. Different languages meet that bar in different ways — I choose one my team can operate confidently for years, not just ship the first feature with.</p>
<p><strong>3. A managed cloud database.</strong> Postgres or MongoDB cover almost every use case I've encountered. I let the cloud provider handle scaling, and I sidestep the "shared database vs. database-per-service" debate entirely, because I don't have services yet.</p>
<p><strong>4. A stateless server.</strong> Single-server doesn't mean single-instance. When a hot spot emerges — say, the user domain — I spin up more instances of the same codebase, point the load balancer at them, and route that domain's traffic to those instances. I've effectively got a dedicated service without rewriting anything. Token-based auth means any request can land on any instance, so I skip sticky sessions entirely.</p>
<p><strong>5. A push-based message bus.</strong> Async work doesn't require a fleet of pods polling a queue. I keep the business logic inside the core, expose it as an API, and configure pub/sub to push messages to that endpoint. Long-running handlers process the work and acknowledge the message.</p>
<p>This gives me message ordering, natural backpressure (the broker slows down when my service is under load), and minimal operational overhead. If a workload is genuinely heavy, I run it on a separate instance of the same monolith dedicated to async tasks.</p>
<h2>Why Most Microservices Are Unnecessary</h2>
<p>Across multiple projects, I've seen the same pattern: most microservices don't earn their keep — usually because they were created without an organizational reason to exist. They were a copy of someone else's architecture, not a reflection of the team that owns them.</p>
<p>An in-process function call is orders of magnitude faster than an HTTP call, and it avoids network latency, retries, and the long tail of distributed-system failures. Building those failure modes into the system before they're justified is the microservices premium I'm not willing to pay early.</p>
<p>In my experience, code is rarely the bottleneck — the database is. Most API work is data manipulation, waiting on a query, or waiting on a third-party service. Time I'd spend scaling microservices is usually better spent scaling the database.</p>
<h2>When a Microservice Actually Earns Its Keep</h2>
<p>Two things justify carving out a service:</p>
<p><strong>Resource isolation.</strong> Some workloads will starve the rest of the system of CPU or memory if they share a process:</p>
<ul>
<li>Email composition, HTML rendering, PDF generation</li>
<li>Image and video processing</li>
<li>AI model inference</li>
</ul>
<p><strong>Team boundaries.</strong> Once a domain is owned by a separate team with its own release cadence, decisions, and on-call rotation, Conway's Law catches up. Forcing two teams to coordinate inside one codebase creates more friction than running two codebases. That's the moment to split — not before.</p>
<h2>Scaling the Organization, Not Just the System</h2>
<p>When I'm leading multiple teams, I still don't start with microservices. I split along domain boundaries first — Conway's Law applied deliberately. Once a team owns a domain, the architecture inside it is their decision; I only care about the contract they expose.</p>
<p>The monolith doesn't survive forever, and that's fine. It survives as long as the team structure justifies it. When the org grows, the system grows with it — but along the seams the team has already drawn, not arbitrary lines I imagined on day one.</p>
<p>My goal is delivering value with the least effort possible, with an architecture that matches the team I have — not the team I'm imagining I'll have someday.</p>
]]></content:encoded></item><item><title><![CDATA[Self-host your Wakatime stats for (almost) free with Supabase Postgres + Google Cloud Run]]></title><description><![CDATA[In my previous post, I introduce the project Wakapi - an alternative to Wakatime but it totally free, it mean you have to self hosted it somewhere.
https://hashnode.com/post/cm3i8c9mp001609ku3rhieuku
 
The database
Wakapi provide several choices of d...]]></description><link>https://nguyengineer.dev/self-host-your-wakatime-stats-for-almost-free-with-supabase-postgres-google-cloud-run</link><guid isPermaLink="true">https://nguyengineer.dev/self-host-your-wakatime-stats-for-almost-free-with-supabase-postgres-google-cloud-run</guid><category><![CDATA[wakatime]]></category><category><![CDATA[wakapi]]></category><category><![CDATA[#cloudrun]]></category><category><![CDATA[google cloud]]></category><category><![CDATA[PostgreSQL]]></category><category><![CDATA[supabase]]></category><dc:creator><![CDATA[Nguyen Engineer]]></dc:creator><pubDate>Fri, 15 Nov 2024 07:54:53 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1731659047290/5da37d5d-be07-4c88-ae3d-e39586163868.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In my previous post, I introduce the project Wakapi - an alternative to Wakatime but it totally free, it mean you have to self hosted it somewhere.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://hashnode.com/post/cm3i8c9mp001609ku3rhieuku">https://hashnode.com/post/cm3i8c9mp001609ku3rhieuku</a></div>
<p> </p>
<h2 id="heading-the-database">The database</h2>
<p>Wakapi provide several choices of database.</p>
<blockquote>
<h3 id="heading-supported-databases">Supported databases</h3>
<p>Wakapi uses <a target="_blank" href="https://gorm.io/">GORM</a> as an ORM. As a consequence, a set of different relational databases is supported.</p>
<ul>
<li><p><a target="_blank" href="https://sqlite.org/">SQLite</a> (<em>default, easy setup</em>)</p>
</li>
<li><p><a target="_blank" href="https://hub.docker.com/_/mysql">MySQL</a> (<em>recommended, because most extensively tested</em>)</p>
</li>
<li><p><a target="_blank" href="https://hub.docker.com/_/mariadb">MariaDB</a> (<em>open-source MySQL alternative</em>)</p>
</li>
<li><p><a target="_blank" href="https://hub.docker.com/_/postgres">Postgres</a> (<em>open-source as well</em>)</p>
</li>
<li><p><a target="_blank" href="https://www.cockroachlabs.com/docs/stable/install-cockroachdb-linux.html">CockroachDB</a> (<em>cloud-native, distributed, Postgres-compatible API</em>)</p>
</li>
<li><p><a target="_blank" href="https://hub.docker.com/_/microsoft-mssql-server">Microsoft SQL Server</a> (<em>Microsoft SQL Server</em>)</p>
</li>
</ul>
</blockquote>
<p>I was looking for a free service to host my database and discovered that the Supabase free tier is sufficient for this small application. They offer a managed Postgres service, which I really appreciate as I love using Postgres.</p>
<p>Get started with:</p>
<ul>
<li><p>Unlimited API requests</p>
</li>
<li><p>50,000 monthly active users</p>
</li>
<li><p>500 MB database space</p>
<p>  Shared CPU • 500 MB RAM</p>
</li>
<li><p>5 GB bandwidth</p>
</li>
<li><p>1 GB file storage</p>
</li>
<li><p>Community support</p>
</li>
</ul>
<p>Supabase is a company, and their need for profit is important. I don't encourage spamming their free tier; we only use it here as an experiment. Also, remember to back up your data because we don’t know when it might be stop offering the free package.</p>
<p>To get started with the free package, follow these steps:</p>
<ol>
<li><p>Visit <a target="_blank" href="https://supabase.com/pricing">https://supabase.com/pricing</a> and register for an account.</p>
</li>
<li><p>Create a project on the platform.</p>
</li>
<li><p>On the home page, click the "Connect" button to obtain the connection string.</p>
</li>
</ol>
<p>Make sure to copy the connection string for the Session mode, as it supports concurrency and is suitable for your app. The Transaction mode is only appropriate for one-time commands.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731653948300/ef0925c2-d2fd-4215-acd4-3e2096423964.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-the-wakapi-app-on-google-cloud">The Wakapi app on Google Cloud</h2>
<p>This is the hard part. You need a credit card to continue 😆</p>
<p><img src="https://i.pinimg.com/736x/ca/42/4c/ca424c76464672e6a9f63bd1039379aa.jpg" alt="Picture memes g7G8wky07 — iFunny" /></p>
<p>However, if you can afford it, it's really cheap.</p>
<p>Cloud Run is incredibly affordable for low workloads. I created a new Google Cloud account to experiment with Kubernetes, but in October I only used a single Cloud Run instance to host my Wakapi. The total cost for that was 2,733 VND, which is approximately $0.11. That's why I say it feels almost free. Please ignore the free trial credit in the image below, as it expires in three months. However, the monthly fee for Cloud Run remains minimal.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731654880154/267c6681-73e0-404f-a0fa-f549e02aa18d.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-how-to-set-up-your-app">How to set up your app</h2>
<p><strong>Steps:</strong></p>
<ol>
<li><p>Pull the original Docker image.</p>
</li>
<li><p>Push it to your Google Cloud Artifact Registry.</p>
</li>
<li><p>Create a Cloud Run instance that uses the most recent image you just pushed.</p>
</li>
<li><p>Set up the necessary configurations.</p>
</li>
</ol>
<p><strong>Pull the docker image</strong></p>
<pre><code class="lang-bash"> docker pull --platform linux/amd64 ghcr.io/muety/wakapi:latest
</code></pre>
<p>Remember to pull this arch <code>--platform linux/amd64</code> If you are on MacOS Apple silicon chip, it will pull the arm64 image and you can’t push it to Artifact register</p>
<p><strong>Push it to your Google Cloud Artifact Registry</strong></p>
<p>After you pull it, your image will be <code>wakapi:latest</code> Now push it to you google project</p>
<pre><code class="lang-bash">docker push gcr.io/YOUR_GOOGLE_PROJECT_ID/wakapi:latest
</code></pre>
<p><strong>Create a Cloud Run instance that uses the most recent image you just pushed.</strong></p>
<p>Create a new Cloud Run Service deploy from container</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731655974389/41e09915-4217-4cdd-b33e-d1749e74528a.png" alt class="image--center mx-auto" /></p>
<p>Give the required setting</p>
<ul>
<li><p>Select the image you just pushed</p>
</li>
<li><p>App entry point</p>
</li>
<li><p>2GiB RAM and 2 CPU is more than enough</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731655922809/a174b6e6-5ca1-4fd7-b2c8-230cee0ea9f2.png" alt class="image--center mx-auto" /></p>
<p><strong>Set up the necessary configurations.</strong></p>
<p>Don’t click Deploy yet, remember your database secret? Wakapi provide all kind of configuration here <a target="_blank" href="https://github.com/muety/wakapi?tab=readme-ov-file#-configuration-options">https://github.com/muety/wakapi?tab=readme-ov-file#-configuration-options</a></p>
<p>You can set it however you want. Here is mine.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731659235449/5e3db3cd-9356-4fa4-8760-00970011179c.png" alt class="image--center mx-auto" /></p>
<p>Example:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Database Configuration</span>
<span class="hljs-built_in">export</span> WAKAPI_DB_AUTOMIGRATE_FAIL_SILENTLY=<span class="hljs-literal">true</span>
<span class="hljs-built_in">export</span> WAKAPI_DB_TYPE=postgres
<span class="hljs-built_in">export</span> WAKAPI_DB_HOST=aws-0-ap-southeast-1.redacted.supabase.co
<span class="hljs-built_in">export</span> WAKAPI_DB_PORT=5432
<span class="hljs-built_in">export</span> WAKAPI_DB_USER=postgres.redacted
<span class="hljs-built_in">export</span> WAKAPI_DB_NAME=postgres
<span class="hljs-built_in">export</span> WAKAPI_DB_PASSWORD=********  <span class="hljs-comment"># Replace with your actual database password</span>

<span class="hljs-comment"># Application Settings</span>
<span class="hljs-built_in">export</span> WAKAPI_LISTEN_IPV4=0.0.0.0
<span class="hljs-built_in">export</span> WAKAPI_ALLOW_SIGNUP=<span class="hljs-literal">false</span>
<span class="hljs-built_in">export</span> WAKAPI_INSECURE_COOKIES=<span class="hljs-literal">true</span>

<span class="hljs-comment"># Security Settings</span>
<span class="hljs-built_in">export</span> WAKAPI_PASSWORD_SALT=********  <span class="hljs-comment"># Replace with your secure random salt</span>
<span class="hljs-built_in">export</span> WAKAPI_SENTRY_DSN=********    <span class="hljs-comment"># Replace with your Sentry DSN if using error tracking</span>

<span class="hljs-comment"># Environment Type</span>
<span class="hljs-built_in">export</span> ENVIRONMENT=production
</code></pre>
<p>The most crucial configuration is the database secret. Once you're satisfied with your settings, click "Deploy" and wait a few minutes for your Cloud Run service to boot up. At the top of the page, you will see a URL; this is your public URL to access your Wakapi. You also have the option to purchase a domain and point it to your Cloud Run service. Googling <code>Cloud Run domain mapping</code> for the further instruction.</p>
<h2 id="heading-setting-up-the-client"><strong>Setting Up the Client</strong></h2>
<p>Wakapi uses the same client as Wakatime, which means that all Wakatime plugins can send data to Wakapi. You have the option to disable data sending to Wakatime and send data exclusively to Wakapi, and that is completely acceptable.</p>
<p>To get started, navigate to your deployed Cloud Run URL and register your first account. Make sure to retrieve the API key and update it in the appropriate location in your system, located at <code>~/.wakatime.cfg</code></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731656753259/ec6b7107-bfbe-4326-98e0-6f0a97db14f0.png" alt class="image--center mx-auto" /></p>
<p>That pretty much, if you run into any issue, let me know in the comment.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Thanks <a target="_self" href="https://wakatime.com/">Wakatime</a> for open source your plugin and thanks <a target="_self" href="https://github.com/muety">Ferdinand Mütsch</a> for making this project.</div>
</div>]]></content:encoded></item><item><title><![CDATA[Wakapi: my first contribution to the open source]]></title><description><![CDATA[I have been using Wakatime since 2017, but I stopped using it in recent years because the free plan, which only provides a two-week range report, is not very helpful. I primarily relied on it for the yearly report to see how much I coded over the pas...]]></description><link>https://nguyengineer.dev/wakapi-my-first-contribution-to-the-open-source</link><guid isPermaLink="true">https://nguyengineer.dev/wakapi-my-first-contribution-to-the-open-source</guid><dc:creator><![CDATA[Nguyen Engineer]]></dc:creator><pubDate>Fri, 15 Nov 2024 04:19:17 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1731640229146/1523069a-22af-4416-8dc3-e869ee80255a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I have been using Wakatime since 2017, but I stopped using it in recent years because the free plan, which only provides a two-week range report, is not very helpful. I primarily relied on it for the yearly report to see how much I coded over the past year, just for fun.</p>
<p>I realized that I was freely providing my valuable coding statistics to them; as a free service, I became the product. I’ve been searching for alternatives and found Wakapi. Wakapi is an open-source project that sends data to your self-hosted server using the same plugins and protocols as Wakatime. It’s quite an ingenious solution. Additionally, since the Wakatime plugin is open-source, we can modify it to send data anywhere we want, and that's perfectly legal.</p>
<p>I set up my server and used it for a while, and I found it fascinating. I'm thinking of contributing to it. Noticed several small problems with this alternative, such as performance issues and a lack of features. So, I found a good first issue from the list to start with:</p>
<p><a target="_blank" href="https://github.com/muety/wakapi/issues">https://github.com/muety/wakapi/issues</a></p>
<p>I'm not familiar with the codebase yet, so working on an issue that requires me to walk through it would be a good starting point, rather than just updating the README. The code is written in Go, and Go developers tend to utilize the standard library as much as possible.</p>
<p>In line with the preferences of the repository owner, I think I can do some refactoring, and I found a suitable ticket for that. The issue isn't difficult, but it does involve a lot of changes. Specifically, it requires replacing the <code>logbuch</code> library with <code>slog</code>. Structured logging was introduced in Go version 1.21, making many structured logging libraries obsolete. Therefore, replacing third-party libraries like <code>logbuch</code> is an obvious choice.</p>
<p>I picked this issue: <a target="_blank" href="https://github.com/muety/wakapi/issues/480">Migrate from <code>logbuch</code> to <code>slog</code> #480</a></p>
<p><a target="_blank" href="https://github.com/muety/wakapi/issues/480"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731642253546/27fc443f-7b17-40de-8b1c-919cb7f7552b.png" alt class="image--center mx-auto" /></a></p>
<p>Collaborating with the repository owner fascinates me as well. After a few back-and-forth PR updates, the pull request was merged. Yay 🎉🎉</p>
<p>Follow up by a few more discussion and additional pull request, the issue is resolve.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731642940227/56a7cb0a-91a4-4c47-baa2-9ed1e27d5d9e.png" alt class="image--center mx-auto" /></p>
<p>Contributing to this issue alone makes me the 9th/50 most significant contributor to the project, possibly due to the LOC changes 😜</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731643095052/d0d64f41-c852-4079-95af-8b4f5ad21c44.png" alt class="image--center mx-auto" /></p>
<p>Thanks to <a target="_blank" href="https://github.com/muety">Ferdinand Mütsch</a>, who is such a nice guy. I still use Wakapi every day and have found many improvements I can implement. I just need to find the time.</p>
<h3 id="heading-my-take-away-to-start-contribute-to-oss">My take away to start contribute to OSS</h3>
<ul>
<li><p><strong>Journey</strong></p>
<p>  Let’s dive into your project. Identify the challenges you face and seek open-source solutions. Utilize these solutions and pinpoint areas for improvement. If you’re familiar with the tech stack, consider reviewing the issue list or submitting an issue ticket to start collaborating with others.</p>
</li>
<li><p><strong>Expertise</strong></p>
<p>  To begin contributing, you need a certain level of expertise. The open-source software (OSS) you develop will be used by many people, potentially thousands or more, running the code you write on their own infrastructure or computers. We don’t want to create issues for them with any new bugs.</p>
</li>
<li><p><strong>Be Proactive</strong></p>
<p>  The time frame for a pull request (PR) should be within a few weeks; otherwise, we risk losing context. While it's great that we are using our free time to create open-source software, maintaining a professional approach is what everyone expects in a collaborative environment. Be proactive and assist the maintainer when needed.</p>
</li>
</ul>
<p>That's all for today. I hope you find inspiration to start your journey as an open source contributor. Here is my GitHub profile; let's connect for more interesting projects: <a target="_blank" href="https://github.com/finnng">https://github.com/finnng</a></p>
]]></content:encoded></item><item><title><![CDATA[Series Building a Chat System that Scales: A Developer's Journey]]></title><description><![CDATA[A Developer's Journey
I've always wanted to build a chat system, just for the joy of it. The original plan was simple: set up an old HP Elitedesk as a server, NAT the ports, point a domain to it, and share it with friends. But as I looked at today's ...]]></description><link>https://nguyengineer.dev/series-building-a-chat-system-that-scales-a-developers-journey</link><guid isPermaLink="true">https://nguyengineer.dev/series-building-a-chat-system-that-scales-a-developers-journey</guid><category><![CDATA[htmx]]></category><category><![CDATA[go-htmx]]></category><category><![CDATA[Go Language]]></category><category><![CDATA[React]]></category><category><![CDATA[templ]]></category><dc:creator><![CDATA[Nguyen Engineer]]></dc:creator><pubDate>Sat, 09 Nov 2024 12:40:43 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1731152494473/e9ad5a0a-c86b-4293-a2a2-1150202bb2c4.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-a-developers-journey">A Developer's Journey</h1>
<p>I've always wanted to build a chat system, just for the joy of it. The original plan was simple: set up an old HP Elitedesk as a server, NAT the ports, point a domain to it, and share it with friends. But as I looked at today's job market, I realized this could be more than just a fun project.</p>
<p>These days, job requirements read like a technical encyclopedia. Companies want developers who can:</p>
<ul>
<li><p>Master frontend technologies from service workers to real-time vanilla JS</p>
</li>
<li><p>Code proficiently in multiple languages (typically Go and JS) with at least 7 years of production experience</p>
</li>
<li><p>Be an expert in both SQL and NoSQL databases, plus columnar databases like Druid or Cassandra for analytics</p>
</li>
<li><p>Handle pub/sub systems like Kafka for microservices</p>
</li>
<li><p>Implement solutions like Debezium or Postgres listen/notify for replication lag</p>
</li>
<li><p>Set up comprehensive monitoring with logging, tracing, and metrics</p>
</li>
<li><p>Deploy and manage Kubernetes clusters (CKA certification preferred)</p>
</li>
<li><p>Build and maintain CI/CD pipelines</p>
</li>
<li><p>And of course, demonstrate experience with systems handling 100M+ requests per day</p>
</li>
</ul>
<p>And that's just to get past the CV screening - we haven't even gotten to the LeetCode challenges yet!</p>
<p>So, I decided to turn my chat system project into a learning journey. I'm setting an ambitious goal: build a system that can handle 100 million requests per day. Not just because it's a common job requirement, but because it's an excellent way to learn these technologies in a practical context.</p>
<p>In this series, I'll document my journey building a scalable chat application from the ground up. We'll cover everything from frontend implementation to deployment and scaling strategies. No shortcuts, no oversimplification - just real, hands-on experience with the tools and techniques that modern tech companies use.</p>
<p>Let's start with where users first interact with our system: the frontend.</p>
<h2 id="heading-the-unexpected-journey-back">The Unexpected Journey Back</h2>
<p>Back in 2012, when I started my career, Node.js was everywhere. The job market was flooded with Node.js opportunities, and I dove straight in. For the next decade, that's where I lived. In doing so, I completely missed the era where people built websites with PHP and jQuery - a gap that would later prove interesting in my HTMX journey.</p>
<h2 id="heading-beyond-pet-projects-building-real-systems">Beyond Pet Projects: Building Real Systems</h2>
<p>Everything changed when I started building a complete system rather than just another small app. Let me tell you - building a system is an entirely different beast from creating "pet" projects where performance, design, and scaling aren't critical concerns. It requires a bird's-eye view while still demanding attention to every line of code.</p>
<p>The scale of the project made me realize something crucial: I needed to minimize the technology stack I had to maintain. Fewer moving parts mean a more stable system. While our backend was solid (even with complex pieces like Kafka, Debezium, Postgres, Centrifugo, Go webserver, and k8s), the frontend remained our Achilles' heel, especially its build process. Despite my seven years of React experience, it still occasionally drives me crazy.</p>
<h2 id="heading-the-frontend-fatigue">The Frontend Fatigue</h2>
<p>Let's talk about our frontend journey - it's quite a tale:</p>
<ul>
<li><p>We needed Babel just to write code with new ES specs</p>
</li>
<li><p>Webpack became our daily wrestling partner</p>
</li>
<li><p>Node.js runtime version upgrades felt like walking through a minefield</p>
</li>
<li><p>Abandoned projects kept us up at night</p>
</li>
<li><p>Deprecated libraries became our regular headache</p>
</li>
<li><p>Security issues? Don't get me started</p>
</li>
</ul>
<p>And let's not forget how OOP implementation in JavaScript was a mess back then. In my opinion, most issues in the JS ecosystem stem from the language's inherent fragility – it created an environment where mistakes could slip by unnoticed.</p>
<h2 id="heading-the-revelation">The Revelation</h2>
<p>This realization led me to reconsider JavaScript's original purpose: a lightweight scripting language to enhance HTML's user experience, bridging the gap between solid backend-rendered HTML and the browser's flexibility. Maybe it was time to put JavaScript back where it belonged – on the client side, in a more focused role.</p>
<p>Here's what this looks like in practice:</p>
<h3 id="heading-before-the-react-way">Before (The React Way):</h3>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> UserProfile = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> [user, setUser] = useState(<span class="hljs-literal">null</span>);
  <span class="hljs-keyword">const</span> [loading, setLoading] = useState(<span class="hljs-literal">true</span>);
  <span class="hljs-comment">// More state management...</span>

  useEffect(<span class="hljs-function">() =&gt;</span> {
    fetchUser()
      .then(<span class="hljs-function"><span class="hljs-params">data</span> =&gt;</span> setUser(data))
      .catch(<span class="hljs-function"><span class="hljs-params">err</span> =&gt;</span> setError(err));
  }, []);

  <span class="hljs-comment">// Complex rendering logic...</span>
};
</code></pre>
<h3 id="heading-after-the-htmx-go-way">After (The HTMX + Go Way):</h3>
<pre><code class="lang-javascript">&lt;!-- Simple, direct, <span class="hljs-attr">effective</span>: get user after the page loaded --&gt;
<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">hx-get</span>=<span class="hljs-string">"/api/user"</span> <span class="hljs-attr">hx-trigger</span>=<span class="hljs-string">"load"</span>&gt;</span>
  <span class="hljs-comment">&lt;!-- Server sends exactly what we need --&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
</code></pre>
<h2 id="heading-learning-htmx-a-personal-challenge">Learning HTMX: A Personal Challenge</h2>
<p>I took HTMX for a test drive in two projects to overcome the honeymoon phenomenon. I'll be honest - it was challenging at first, mainly because of my background. Remember how I mentioned missing the PHP and jQuery era? That gap became apparent. Early in my career, I was deep in mobile app and game development, working with React and React Native, before transitioning directly into backend development.</p>
<p>So, when it came to HTML-over-the-wire, it felt like learning to write with my left hand. Sending partial HTML and using a dedicated API for form validation? Updating the UI via a backend API? It all felt strange initially. But here's the thing - the more I work with it, the more I understand how it makes sense. It isn't that it's difficult; it's that my thinking was conditioned to use the more complicated way.</p>
<h2 id="heading-why-go-htmx-clicks">Why Go + HTMX Clicks</h2>
<p>Here's what I've discovered works brilliantly:</p>
<ol>
<li><p><strong>One Model to Rule Them All</strong></p>
<ul>
<li><p>No more juggling between frontend and backend models</p>
</li>
<li><p>No more omitting fields before sending to the frontend</p>
</li>
<li><p>Everything lives where it should - on the server</p>
</li>
</ul>
</li>
<li><p><strong>Go's Superpowers in Frontend Code</strong></p>
<ul>
<li><p>Imagine writing frontend code with Go's compiler watching your back</p>
</li>
<li><p>If it builds, most issues are already caught</p>
</li>
<li><p>No more undefined/null/empty string gymnastics</p>
</li>
</ul>
</li>
<li><p><strong>Development Joy</strong></p>
<p> Instead of:</p>
<pre><code class="lang-javascript"> <span class="hljs-comment">// Dealing with JavaScript uncertainty</span>
 <span class="hljs-keyword">const</span> userEmail = user &amp;&amp; user.email || <span class="hljs-string">''</span>;
</code></pre>
<p> We get (this is <code>Go templ</code> syntax):</p>
<pre><code class="lang-go"> &lt;span&gt;{ User.Email }&lt;/span&gt;
</code></pre>
<p> Because static typed language already have the solid default value. User.Email is string so the default value is <code>““</code>. No more <code>null</code>, <code>undefined</code>, <code>‘‘</code>. madness.</p>
</li>
</ol>
<h2 id="heading-real-world-impact">Real-World Impact</h2>
<p>In our production environment, this approach has meant:</p>
<ul>
<li><p>Dramatically simpler deployment process</p>
</li>
<li><p>Faster feature implementation</p>
</li>
<li><p>Fewer moving parts to maintain</p>
</li>
<li><p>Better sleep at night (seriously!)</p>
</li>
</ul>
<h2 id="heading-looking-forward">Looking Forward</h2>
<p>This journey has taught me that sometimes, simpler really is better. While this doesn't mean we should abandon React or other frontend frameworks entirely – it depends on your needs – it's shown me a more sustainable path for certain types of applications.</p>
<p>In my next post, I'll dive deep into a real project built with this stack. I'll share the nitty-gritty details:</p>
<ul>
<li><p>How we structured our templates</p>
</li>
<li><p>Where HTMX really shines</p>
</li>
<li><p>The challenges we faced and overcame</p>
</li>
<li><p>Practical patterns we discovered along the way</p>
</li>
</ul>
<p>The web development world keeps evolving, and sometimes evolution means rediscovering what we left behind. Stay tuned for more concrete examples and detailed code walkthrough!</p>
]]></content:encoded></item><item><title><![CDATA[Replace string contains special character in Vim]]></title><description><![CDATA[A Simple Solution to a Tricky Problem
If you've ever tried to replace a string containing special characters in Vim, especially across multiple files, you know it can be a real headache. The usual search and replace commands often fall short, getting...]]></description><link>https://nguyengineer.dev/replace-string-contains-special-character-in-vim</link><guid isPermaLink="true">https://nguyengineer.dev/replace-string-contains-special-character-in-vim</guid><category><![CDATA[vim]]></category><category><![CDATA[neovim]]></category><dc:creator><![CDATA[Nguyen Engineer]]></dc:creator><pubDate>Wed, 16 Oct 2024 13:00:15 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/SWfcRVm-o0E/upload/a828a2327f2fff146b4e12181187e415.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>A Simple Solution to a Tricky Problem</p>
<p>If you've ever tried to replace a string containing special characters in Vim, especially across multiple files, you know it can be a real headache. The usual search and replace commands often fall short, getting tripped up by those pesky special characters.</p>
<p>After much trial and error, I've found a reliable method that makes this task straightforward. Here's how to do it:</p>
<ol>
<li><p>First, use fzf's live grep to find all occurrences of your string:</p>
<pre><code class="lang-bash"> :Rg <span class="hljs-string">"w.Header().Set(\"Content-Type\", \"text/html; charset=utf-8\")"</span>
</code></pre>
<p> This populates your quickfix list with all matches.</p>
</li>
<li><p>Now, here's the key command that does the heavy lifting:</p>
<pre><code class="lang-bash"> :cfdo %s/\V\Cw.Header().Set(<span class="hljs-string">"Content-Type"</span>, <span class="hljs-string">"text\/html; charset=utf-8"</span>)/w.Header().Set(<span class="hljs-string">"Content-Type"</span>, <span class="hljs-string">"application\/json"</span>)/g
</code></pre>
<p> Let's break it down:</p>
<ul>
<li><p><code>cfdo</code>: Applies the command to all files in the quickfix list.</p>
</li>
<li><p><code>\V</code>: "Very nomagic" mode, which treats most characters literally.</p>
</li>
<li><p><code>\C</code>: Ensures case-sensitive matching.</p>
</li>
<li><p><code>\/</code>: Escapes forward slashes to avoid confusion.</p>
</li>
</ul>
</li>
<li><p>Finally, save all your changes:</p>
<pre><code class="lang-bash"> :wa
</code></pre>
</li>
</ol>
<p>And there you have it! This method reliably replaces your string, special characters and all, across your entire project.</p>
<p>No more wrestling with escape characters or pulling your hair out over missed replacements. With this approach, you can handle even the trickiest of string replacements in Vim with ease.</p>
<p>Give it a try next time you're faced with a challenging find-and-replace task. You might be surprised at how smoothly it goes!</p>
]]></content:encoded></item><item><title><![CDATA[Connect Cloud Run Services to VPC via Terraform]]></title><description><![CDATA[Introduction
In this guide, we'll walk through setting up two Cloud Run services - a public frontend and a private backend - using Terraform. We'll use Google Artifact Registry to store our Docker images. This setup demonstrates how to create a secur...]]></description><link>https://nguyengineer.dev/connect-cloud-run-services-to-vpc-via-terraform</link><guid isPermaLink="true">https://nguyengineer.dev/connect-cloud-run-services-to-vpc-via-terraform</guid><category><![CDATA[Terraform]]></category><category><![CDATA[#cloudrun]]></category><category><![CDATA[gcloud]]></category><category><![CDATA[Go Language]]></category><category><![CDATA[vpc]]></category><dc:creator><![CDATA[Nguyen Engineer]]></dc:creator><pubDate>Wed, 28 Aug 2024 01:58:52 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1724810153301/0ad78723-8a40-4b08-899a-b21d2829065c.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>In this guide, we'll walk through setting up two Cloud Run services - a public frontend and a private backend - using Terraform. We'll use Google Artifact Registry to store our Docker images. This setup demonstrates how to create a secure, scalable microservices architecture on Google Cloud Platform.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<ul>
<li><p>A Google Cloud Platform account</p>
</li>
<li><p><code>gcloud</code> CLI installed and configured</p>
</li>
<li><p>Terraform installed</p>
</li>
<li><p>Docker installed</p>
</li>
<li><p>Git installed</p>
</li>
</ul>
<h2 id="heading-getting-started">Getting Started</h2>
<ol>
<li><p>Clone my demo repository:</p>
<pre><code class="lang-yaml"> <span class="hljs-string">git</span> <span class="hljs-string">clone</span> <span class="hljs-string">git@github.com:finnng/demo-cloud-run-vpc.git</span>
 <span class="hljs-string">cd</span> <span class="hljs-string">demo-cloud-run-vpc</span>
</code></pre>
</li>
<li><p>Set up your Google Cloud project:</p>
<pre><code class="lang-yaml"> <span class="hljs-string">export</span> <span class="hljs-string">PROJECT_ID=your-project-id</span>
 <span class="hljs-string">gcloud</span> <span class="hljs-string">config</span> <span class="hljs-string">set</span> <span class="hljs-string">project</span> <span class="hljs-string">$PROJECT_ID</span>
</code></pre>
</li>
<li><p>Enable necessary APIs:</p>
<pre><code class="lang-yaml"> <span class="hljs-string">gcloud</span> <span class="hljs-string">services</span> <span class="hljs-string">enable</span> <span class="hljs-string">run.googleapis.com</span> <span class="hljs-string">artifactregistry.googleapis.com</span> <span class="hljs-string">compute.googleapis.com</span>
</code></pre>
</li>
<li><p>Create a Google Artifact Registry repository:</p>
<pre><code class="lang-yaml"> <span class="hljs-string">gcloud</span> <span class="hljs-string">artifacts</span> <span class="hljs-string">repositories</span> <span class="hljs-string">create</span> <span class="hljs-string">cloud-run-demo</span> <span class="hljs-string">--repository-format=docker</span> <span class="hljs-string">--location=us-central1</span>
</code></pre>
</li>
</ol>
<h2 id="heading-building-and-pushing-docker-images">Building and Pushing Docker Images</h2>
<ol>
<li><p>Build and push the backend image:</p>
<pre><code class="lang-yaml"> <span class="hljs-string">cd</span> <span class="hljs-string">backend</span>
 <span class="hljs-string">docker</span> <span class="hljs-string">build</span> <span class="hljs-string">--platform</span> <span class="hljs-string">linux/amd64</span> <span class="hljs-string">-t</span> <span class="hljs-string">us-central1-docker.pkg.dev/$PROJECT_ID/cloud-run-demo/backend:v1</span> <span class="hljs-string">.</span>
 <span class="hljs-string">docker</span> <span class="hljs-string">push</span> <span class="hljs-string">us-central1-docker.pkg.dev/$PROJECT_ID/cloud-run-demo/backend:v1</span>
 <span class="hljs-string">cd</span> <span class="hljs-string">..</span>
</code></pre>
</li>
<li><p>Build and push the frontend image:</p>
<pre><code class="lang-yaml"> <span class="hljs-string">cd</span> <span class="hljs-string">frontend</span>
 <span class="hljs-string">docker</span> <span class="hljs-string">build</span> <span class="hljs-string">--platform</span> <span class="hljs-string">linux/amd64</span> <span class="hljs-string">-t</span> <span class="hljs-string">us-central1-docker.pkg.dev/$PROJECT_ID/cloud-run-demo/frontend:v1</span> <span class="hljs-string">.</span>
 <span class="hljs-string">docker</span> <span class="hljs-string">push</span> <span class="hljs-string">us-central1-docker.pkg.dev/$PROJECT_ID/cloud-run-demo/frontend:v1</span>
 <span class="hljs-string">cd</span> <span class="hljs-string">..</span>
</code></pre>
</li>
</ol>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Remember to choose the platform <code>--platform linux/amd64</code> Especially when you are using Mac M1 chip.</div>
</div>

<h2 id="heading-configuring-terraform">Configuring Terraform</h2>
<ol>
<li><p>Update <code>terraform.tfvars</code> with your project details:</p>
<pre><code class="lang-yaml"> <span class="hljs-string">project_id</span>     <span class="hljs-string">=</span> <span class="hljs-string">"your-project-id"</span>
 <span class="hljs-string">region</span>         <span class="hljs-string">=</span> <span class="hljs-string">"us-central1"</span>
 <span class="hljs-string">frontend_image</span> <span class="hljs-string">=</span> <span class="hljs-string">"us-central1-docker.pkg.dev/your-project-id/cloud-run-demo/frontend:v1"</span>
 <span class="hljs-string">backend_image</span>  <span class="hljs-string">=</span> <span class="hljs-string">"us-central1-docker.pkg.dev/your-project-id/cloud-run-demo/backend:v1"</span>
</code></pre>
</li>
<li><p>Initialize Terraform:</p>
<pre><code class="lang-yaml"> <span class="hljs-string">terraform</span> <span class="hljs-string">init</span>
</code></pre>
</li>
<li><p>Plan and apply the Terraform configuration:</p>
<pre><code class="lang-yaml"> <span class="hljs-string">terraform</span> <span class="hljs-string">plan</span>
 <span class="hljs-string">terraform</span> <span class="hljs-string">apply</span>
</code></pre>
</li>
</ol>
<h2 id="heading-understanding-the-configuration">Understanding the Configuration</h2>
<p>Let's look at some key parts of our Terraform configuration:</p>
<h3 id="heading-vpc-and-connector-setup">VPC and Connector Setup</h3>
<pre><code class="lang-yaml"><span class="hljs-string">resource</span> <span class="hljs-string">"google_compute_network"</span> <span class="hljs-string">"vpc_network"</span> {
  <span class="hljs-string">name</span>                    <span class="hljs-string">=</span> <span class="hljs-string">"cloud-run-network"</span>
  <span class="hljs-string">auto_create_subnetworks</span> <span class="hljs-string">=</span> <span class="hljs-literal">false</span>
}

<span class="hljs-string">resource</span> <span class="hljs-string">"google_vpc_access_connector"</span> <span class="hljs-string">"connector"</span> {
  <span class="hljs-string">name</span>          <span class="hljs-string">=</span> <span class="hljs-string">"vpc-con"</span>
  <span class="hljs-string">ip_cidr_range</span> <span class="hljs-string">=</span> <span class="hljs-string">"10.8.0.0/28"</span>
  <span class="hljs-string">network</span>       <span class="hljs-string">=</span> <span class="hljs-string">google_compute_network.vpc_network.name</span>
  <span class="hljs-string">region</span>        <span class="hljs-string">=</span> <span class="hljs-string">var.region</span>
}
</code></pre>
<p>This creates a VPC network and a connector, allowing our Cloud Run services to communicate securely.</p>
<h3 id="heading-backend-service">Backend Service</h3>
<pre><code class="lang-yaml"><span class="hljs-string">resource</span> <span class="hljs-string">"google_cloud_run_v2_service"</span> <span class="hljs-string">"backend"</span> {
  <span class="hljs-string">name</span>     <span class="hljs-string">=</span> <span class="hljs-string">"backend"</span>
  <span class="hljs-string">location</span> <span class="hljs-string">=</span> <span class="hljs-string">var.region</span>

  <span class="hljs-string">template</span> {
    <span class="hljs-string">containers</span> {
      <span class="hljs-string">image</span> <span class="hljs-string">=</span> <span class="hljs-string">var.backend_image</span>
    }
    <span class="hljs-string">service_account</span> <span class="hljs-string">=</span> <span class="hljs-string">google_service_account.demo_backend_sa.email</span>
    <span class="hljs-string">vpc_access</span> {
      <span class="hljs-string">connector</span> <span class="hljs-string">=</span> <span class="hljs-string">google_vpc_access_connector.connector.id</span>
      <span class="hljs-string">egress</span>    <span class="hljs-string">=</span> <span class="hljs-string">"PRIVATE_RANGES_ONLY"</span>
    }
  }

  <span class="hljs-string">ingress</span> <span class="hljs-string">=</span> <span class="hljs-string">"INGRESS_TRAFFIC_INTERNAL_ONLY"</span>
}
</code></pre>
<p>Note the <code>egress = "PRIVATE_RANGES_ONLY"</code> setting, which allows the backend to make external API calls while maintaining security.</p>
<h3 id="heading-frontend-service">Frontend Service</h3>
<pre><code class="lang-yaml"><span class="hljs-string">resource</span> <span class="hljs-string">"google_cloud_run_v2_service"</span> <span class="hljs-string">"frontend"</span> {
  <span class="hljs-string">name</span>     <span class="hljs-string">=</span> <span class="hljs-string">"frontend"</span>
  <span class="hljs-string">location</span> <span class="hljs-string">=</span> <span class="hljs-string">var.region</span>

  <span class="hljs-string">template</span> {
    <span class="hljs-string">containers</span> {
      <span class="hljs-string">image</span> <span class="hljs-string">=</span> <span class="hljs-string">var.frontend_image</span>
      <span class="hljs-string">env</span> {
        <span class="hljs-string">name</span>  <span class="hljs-string">=</span> <span class="hljs-string">"BACKEND_URL"</span>
        <span class="hljs-string">value</span> <span class="hljs-string">=</span> <span class="hljs-string">google_cloud_run_v2_service.backend.uri</span>
      }
    }
    <span class="hljs-string">service_account</span> <span class="hljs-string">=</span> <span class="hljs-string">google_service_account.demo_frontend_sa.email</span>
    <span class="hljs-string">vpc_access</span> {
      <span class="hljs-string">connector</span> <span class="hljs-string">=</span> <span class="hljs-string">google_vpc_access_connector.connector.id</span>
      <span class="hljs-string">egress</span>    <span class="hljs-string">=</span> <span class="hljs-string">"ALL_TRAFFIC"</span>
    }
  }
}
</code></pre>
<p>The frontend service is configured to access the backend securely through the VPC connector.</p>
<h2 id="heading-testing-the-setup">Testing the Setup</h2>
<p>After applying the Terraform configuration, you can test your setup:</p>
<ol>
<li><p>Get the URLs:</p>
<pre><code class="lang-yaml"> <span class="hljs-string">terraform</span> <span class="hljs-string">output</span> <span class="hljs-string">frontend_url</span>
 <span class="hljs-string">terraform</span> <span class="hljs-string">output</span> <span class="hljs-string">backend_url</span>
</code></pre>
</li>
<li><p>Open the URL in a web browser. You should see the frontend HTML, that means the frontend is publicly access.</p>
</li>
<li><p>To verify the backend is private, try accessing its URL directly (you can find it in the GCP Console). It should not be publicly accessible.</p>
</li>
<li><p>Now try to let call https://{frontend_url}/request-backend. This request is call to backend and backend make an external call to https://example.com and return the html. You should see the website content of example.com</p>
</li>
</ol>
<h2 id="heading-cleaning-up">Cleaning Up</h2>
<p>To avoid incurring charges, remember to destroy the resources when you're done:</p>
<pre><code class="lang-yaml"><span class="hljs-string">terraform</span> <span class="hljs-string">destroy</span>
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>This guide demonstrates how to set up a secure microservices architecture using Cloud Run and Terraform. By keeping the backend private and allowing the frontend to communicate with it through a VPC, we've created a scalable and secure setup.</p>
<p>Remember to always consider security best practices, especially in production environments. Regularly review and update your configurations to maintain cloud security standards.</p>
<p>Happy coding, and enjoy exploring Cloud Run and Terraform!</p>
]]></content:encoded></item><item><title><![CDATA[Building a data analytic Slack App with Machine Learning]]></title><description><![CDATA[Clustering Slack alerts with TF-IDF and DBSCAN
Most teams have a Slack channel full of alerts that everyone eventually mutes. When something breaks, the relevant message is buried under hundreds of co]]></description><link>https://nguyengineer.dev/building-a-data-analytic-slack-app-with-machine-learning</link><guid isPermaLink="true">https://nguyengineer.dev/building-a-data-analytic-slack-app-with-machine-learning</guid><category><![CDATA[golang]]></category><category><![CDATA[Machine Learning]]></category><category><![CDATA[slack]]></category><category><![CDATA[Cloud]]></category><category><![CDATA[Python]]></category><dc:creator><![CDATA[Nguyen Engineer]]></dc:creator><pubDate>Sun, 04 Aug 2024 13:00:35 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/bmmcfZqSjBU/upload/58df1ea428fbf521a84c6075cbfb6f14.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>Clustering Slack alerts with TF-IDF and DBSCAN</h1>
<p>Most teams have a Slack channel full of alerts that everyone eventually mutes. When something breaks, the relevant message is buried under hundreds of copies of the same timeout warning and a pod that's been crash-looping for days.</p>
<p>The noise is repetitive. The same handful of errors repeat, with a timestamp and a pod name swapped in. Repetitive data is compressible, and "group the near-duplicates and show one of each" is a clustering problem with well-established solutions.</p>
<p>I built Alligator, a Slack app that reads a channel over a time range and returns a report that collapses 2,000 alerts into the handful of distinct problems behind them. The Slack integration is the presentation layer. This post is mostly about the ML.</p>
<h2>Why not an LLM</h2>
<p>Feeding a few thousand log lines to an LLM to summarize the errors works, but it costs money per run, sends production logs to a third party, and returns a different answer each time. The task here is deduplication, and classic ML handles it well: TF-IDF to turn text into vectors, DBSCAN to group the vectors. Both run locally in a couple of seconds, cost nothing, and are deterministic.</p>
<h2>The ML pipeline</h2>
<p>Take the text of every message in the channel, turn each one into a vector, then cluster the vectors so near-identical messages group together. Three steps: vectorize, cluster, read off the groups.</p>
<h3>Step 1 — Text into vectors with TF-IDF</h3>
<p>Clustering works on numbers, not strings. TF-IDF (Term Frequency–Inverse Document Frequency) converts a set of documents into vectors:</p>
<ul>
<li>Term frequency weights words by how often they appear in a given message.</li>
<li>Inverse document frequency down-weights words that appear in most messages. If every line contains <code>error</code>, then <code>error</code> carries no information about which cluster a message belongs to.</li>
</ul>
<p>The result is that the terms distinguishing one kind of alert from another get the most weight, which is what clustering needs.</p>
<p>The implementation is a few lines with scikit-learn:</p>
<pre><code class="language-python">from sklearn.feature_extraction.text import TfidfVectorizer

def vectorize_logs(logs):
    vectorizer = TfidfVectorizer(max_features=1000)
    return vectorizer.fit_transform(logs)
</code></pre>
<h4>Preprocessing: the standard pipeline and what this problem needs</h4>
<p>The standard NLP preprocessing pipeline has five steps:</p>
<ol>
<li>Lowercase, so <code>Timeout</code> and <code>timeout</code> count as the same word.</li>
<li>Strip punctuation, so <code>timeout,</code> and <code>timeout</code> match.</li>
<li>Tokenize, splitting each string into individual words.</li>
<li>Remove stop words: high-frequency filler like <code>the</code>, <code>is</code>, <code>a</code>.</li>
<li>Stem, reducing words to their root so <code>running</code>, <code>runs</code>, and <code>ran</code> collapse to <code>run</code>.</li>
</ol>
<p><code>TfidfVectorizer</code> performs steps 1–3 by default: it lowercases, strips punctuation, and tokenizes on word boundaries with no extra code. This project relies on those defaults and skips steps 4 and 5.</p>
<p>Stop-word removal targets English prose. Log lines are not prose; they are strings like <code>CrashLoopBackOff</code>, <code>connection refused</code>, and <code>context deadline exceeded</code>. Standard stop-word lists rarely match anything in this data, and the terms that actually saturate it (<code>error</code>, <code>warning</code>, <code>failed</code>) are already down-weighted by IDF. A stop-word pass would be redundant here.</p>
<p>Stemming helps when matching natural language that inflects, such as <code>running</code> versus <code>ran</code>. Log lines repeat near-verbatim and do not inflect, so stemming adds a dependency and risks mangling identifiers (<code>Booting</code> and <code>booted</code> both collapse to <code>boot</code>) with no gain in cluster quality.</p>
<p>The point is to run the full pipeline against the data at hand and keep the steps that improve results. A log clusterer and a sentiment classifier start from the same five steps and keep different ones.</p>
<p><code>max_features=1000</code> caps the vocabulary at the 1,000 highest-signal terms. Logs have a long tail of unique tokens, since every UUID and timestamp is new, and without a cap the vectors grow to tens of thousands of low-value dimensions. Capping keeps clustering fast and the vectors focused on terms that recur.</p>
<p>The output is a sparse matrix, one row per message and one column per term. A given alert contains only a handful of the 1,000 terms, so storing the zeros would be wasteful. scikit-learn keeps the matrix sparse end to end, which is part of why this is fast.</p>
<p>One domain-specific step would help more than any textbook one: normalizing log tokens before vectorizing. Right now <code>pod-7f8a9</code> and <code>pod-3b2c1</code> are distinct tokens, which pushes otherwise-identical alerts apart. Masking the dynamic parts (timestamps to <code>&lt;TS&gt;</code>, UUIDs to <code>&lt;UUID&gt;</code>, numbers to <code>&lt;NUM&gt;</code>) collapses them into one token and tightens every cluster. It is domain-specific, which is why it is not on the generic list.</p>
<h3>Step 2 — Grouping with DBSCAN</h3>
<p>With the vectors ready, the next step is grouping them. k-means is the common default and is a poor fit here for two reasons.</p>
<p>First, it requires the number of clusters up front. The number of distinct problems in the channel is the unknown the report is meant to surface. Setting <code>k=5</code> when there are 12 problems merges unrelated errors.</p>
<p>Second, k-means assigns every point to a cluster. DBSCAN does not. It is density-based: it grows a cluster only where points are packed closely together, and points without enough close neighbors are labeled noise (<code>-1</code>).</p>
<p>For this problem the noise label is useful output, not discarded data. The 2,000 repeated timeouts form a dense cluster that can be collapsed. A single <code>panic: nil pointer dereference</code> that appeared once has no dense neighborhood, so it lands in noise. That singleton is the rare, un-triaged event worth a human's attention. DBSCAN separates the repetitive bulk from the one-offs.</p>
<p>The clustering call:</p>
<pre><code class="language-python">from sklearn.cluster import DBSCAN

def cluster_logs_to_labels(X, eps=0.5, min_samples=5):
    db = DBSCAN(eps=eps, min_samples=min_samples, metric="cosine", n_jobs=-1).fit(X)
    return db.labels_
</code></pre>
<p>Three parameters carry the weight:</p>
<ul>
<li><code>metric="cosine"</code>. Euclidean distance is the wrong choice for TF-IDF vectors. A long log line and a short one that mean the same thing sit far apart in Euclidean space because one has more words. Cosine distance measures the angle between vectors, comparing which terms a message emphasizes rather than its length. For text, cosine is usually the right metric.</li>
<li><code>eps=0.5</code>. The distance threshold for two points to count as neighbors. With cosine distance the range is 0 (identical) to 1 (nothing in common). Lower values produce stricter, more numerous clusters; higher values merge everything into one. <code>0.5</code> worked on this data: loose enough to catch the same error across different pods, tight enough to keep distinct problems apart.</li>
<li><code>min_samples</code>. How many neighbors a point needs to anchor a cluster. Production runs at 3, so an alert has to fire at least three times to count as a recurring pattern. The benchmark uses 5. This is the first parameter to adjust when tuning.</li>
</ul>
<p><code>n_jobs=-1</code> uses all available cores.</p>
<p>The rest is bookkeeping: walk the labels, drop each message into its cluster, and send everything labeled <code>-1</code> to the noise list.</p>
<pre><code class="language-python">clusters = {}
noise = []

for label, log in zip(labels, logs):
    if label == -1:
        noise.append(log)
    else:
        clusters.setdefault(str(label), []).append(log)
</code></pre>
<p>That is the ML core: two scikit-learn calls and a loop. No training run, no GPU, no model to host, no API key. It processes a channel in seconds and returns the same result every time.</p>
<h2>Wiring Python into a Go service</h2>
<p>The app is written in Go, which suits a small concurrent HTTP service. The ML lives in Python because that is where the mature libraries are. The two connect without a microservice, gRPC, or a second container. The Go service runs the Python script as a subprocess and pipes JSON over stdin and stdout.</p>
<pre><code class="language-go">cmd := exec.Command(pythonPath, pythonScriptPath)
cmd.Stdin = bytes.NewBuffer(inputJSON)

var out, stderr bytes.Buffer
cmd.Stdout = &amp;out
cmd.Stderr = &amp;stderr

if err := cmd.Run(); err != nil {
    return nil, fmt.Errorf("error running Python script: %v\nStderr: %s", err, stderr.String())
}

var output ClusterResult
if err := json.Unmarshal(out.Bytes(), &amp;output); err != nil {
    return nil, err
}
</code></pre>
<p>The Python side reads JSON from stdin, writes JSON to stdout, and sends all logging to stderr so it never corrupts the data channel:</p>
<pre><code class="language-python">input_data = json.loads(sys.stdin.read())
logs = input_data["logs"]

X = vectorize_logs(logs)
labels = cluster_logs_to_labels(X, input_data.get("eps", 0.5),
                                   input_data.get("min_samples", 5))

# progress and debug info goes to stderr, results go to stdout
print(f"Processing {len(logs)} logs", file=sys.stderr)
json.dump(output, sys.stdout)
</code></pre>
<p>The workload is a batch job that runs for a few seconds and exits. A subprocess with a JSON contract is simple to reason about and to test (<code>echo '{...}' | python tfidf_dbscan.py</code>), and it ships both languages in one container, with the Go binary and Python venv side by side. No service mesh, no second deploy.</p>
<h3>Why the split, and why start in Go</h3>
<p>The project had two goals: solve the alert-noise problem, and understand TF-IDF and DBSCAN well enough to implement them. So I started in Go and wrote both algorithms by hand.</p>
<p>That path taught me the fundamentals. Implementing TF-IDF and DBSCAN from scratch forces you to work through the sparse vectors, the distance metric, and the neighborhood expansion rather than treat them as a library call. It also stalled. Go has limited tooling for vector and matrix computation, so I was building the numeric primitives myself, and the implementation was dense, naive, and single-threaded. Processing about 4MB of logs took nine minutes. The equivalent Python runs in seconds.</p>
<p>At that point I had what I wanted from writing it by hand. The remaining work was optimization that scikit-learn already does: its TF-IDF and DBSCAN are years of tuned, vectorized, sparse-matrix-aware C behind a Python API. Rewriting that in Go was not the goal; solving the problem was. Switching the ML to Python did both, so the project succeeded on both counts.</p>
<p>For numerical and ML work, the library ecosystem often matters more than the language. Python's libraries are fast because the scientific-computing community has spent decades optimizing them.</p>
<h2>The Slack app</h2>
<p>The Slack integration is thin. The flow:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1722775174421/98da6b53-80b9-4d5b-906b-dd8a6983b905.png" alt="Slack app pipeline" /></p>
<ol>
<li>A user triggers Alligator in a channel. The app opens a modal, built with Slack's Block Kit, asking for a start and end time.</li>
<li>On submit, the service fetches the channel history for that window via <code>conversations.history</code>.</li>
<li>Clustering takes a few seconds and Slack expects a response within three, so the service returns a link to a report that is not ready yet and runs the work in a background goroutine.</li>
<li>When clustering finishes, the report is rendered to HTML and written to a Cloud Storage bucket. The user opens the link to see the clusters.</li>
</ol>
<p>The respond-now, compute-later split is the part of the Slack integration worth reusing. Slack's 3-second timeout means anything slower than trivial has to run asynchronously:</p>
<pre><code class="language-go">// respond immediately with a link to the report, which is not ready yet
api.PostEphemeral(channelId, userID, slack.MsgOptionText(
    fmt.Sprintf("Processing analytic. Checkout the report at %s/reports/%s",
        hostURL, reportKey), false))

// run the clustering in the background
go func() {
    output, err := analytic.RunClustering(converted, 0.5, 3)
    if err != nil {
        slog.Error("Error analyzing texts", "error", err)
        return
    }
    report.RenderReport(reportKey, *output)
}()
</code></pre>
<p>The report page is server-rendered with <a href="https://templ.guide/">templ</a>. It sorts clusters by size so the loudest problems appear first, shows one sample per cluster with the rest behind a collapsible, and lists the noise separately.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1722775931870/1913aaa6-b303-493c-a774-04884f26fecd.png" alt="Clustering result" /></p>
<h2>Deployment</h2>
<p>The service runs on Cloud Run, deployed from <code>master</code>. A push triggers Cloud Build, which builds the single Go and Python image, pushes it to Artifact Registry, and Cloud Run rolls it out with zero downtime. Reports live in a Cloud Storage bucket with a 7-day lifecycle so old ones expire on their own.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1722775491055/b56176f6-4221-41d7-8b32-202689a2ca63.png" alt="GCP architecture" /></p>
<h2>Takeaways</h2>
<ul>
<li>For deduplication, TF-IDF and DBSCAN beat an LLM on cost, speed, privacy, and reproducibility. The task defines the tool.</li>
<li>DBSCAN's noise label is useful output for triage. An algorithm that forces every point into a cluster discards the rare one-off, which is often the message worth reading.</li>
<li>For numerical work, the library ecosystem is a first-order concern. The nine-minutes-to-seconds difference came from scikit-learn's optimized C, not from the choice of Python.</li>
<li>A subprocess with a JSON contract kept two languages in one container with no orchestration. It is worth replacing only when it causes a concrete problem.</li>
</ul>
<h2>What's next</h2>
<p>The highest-value change is log-token normalization: masking timestamps, UUIDs, and numbers before vectorizing to tighten clusters. After that, feeding each cluster rather than the full stream to an LLM for a one-line summary. Cheap, deterministic ML compresses 2,000 lines into a handful of groups, and the model only sees the groups, which keeps the cost low.</p>
<p>Beyond that: larger time windows, better monitoring, and a smoother install for the Slack marketplace.</p>
]]></content:encoded></item><item><title><![CDATA[My problem-solving frameworks]]></title><description><![CDATA[A problem-solving framework is a checklist of questions that help you break down a problem and figure out how to solve it. It's like a step-by-step guide to understanding the issue, brainstorming solutions, picking the best one, and then implementing...]]></description><link>https://nguyengineer.dev/unlock-the-mystery-how-top-engineers-use-frameworks-to-solve-problems-faster</link><guid isPermaLink="true">https://nguyengineer.dev/unlock-the-mystery-how-top-engineers-use-frameworks-to-solve-problems-faster</guid><category><![CDATA[#softwareengineering]]></category><category><![CDATA[problem solving skills]]></category><category><![CDATA[framework]]></category><dc:creator><![CDATA[Nguyen Engineer]]></dc:creator><pubDate>Wed, 10 Apr 2024 23:50:45 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1732763417506/f2d19663-3b47-4cd5-9d6a-2fbb94b4ce9f.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>A</em> <strong><em>problem-solving framework</em></strong> <em>is a checklist of questions that help you break down a problem and figure out how to solve it. It's like a step-by-step guide to understanding the issue, brainstorming solutions, picking the best one, and then implementing that solution.</em></p>
<p>As software engineers, our day-to-day tasks are mostly problem-solving. Given a problem, we find the solution. We gather a lot of skills and practice, and time after time, we become proficient with it. We automatically follow some frameworks but don't realize it.</p>
<p>One of the challenges of an organization is not to depend on the key person, that person who has gathered a lot of knowledge and practice. He can resolve the problem easily; he's a superhero, a firefighter. But if he leaves the company, all of that is lost.</p>
<p>Documentation won't help much; it will be outdated and no one will look at it.</p>
<p>IMO, I think we should focus on building the process, not the key person.</p>
<p>A well-defined process can help the newcomer go through a checklist with only the basic, fundamental knowledge about the system. In case of an emergency, a team member without experience can try the process for the first time, it works for them and if anything is missing from their perspective they can add it to the process.</p>
<p>To be more specific, we call the process a framework. The framework is opinionated by the creator. A place where we can see how the top performers work, extract their methodologies, turn them into a process, and make the excellent a standard for everyone.</p>
<h2 id="heading-example-framework-to-investigate-a-bug">Example: Framework to Investigate a Bug</h2>
<p>If we have a bug report that something went wrong in the last 3 months, what should we do?</p>
<ol>
<li><p>Understand the problem, and read the problem statement at least twice to ensure the reported issue is correct</p>
</li>
<li><p>If the resolver lacks knowledge, find the help page, find the document, or consult another team member to understand how it works</p>
</li>
<li><p>Define the clear expectation</p>
</li>
<li><p>Dig into the log, understand why the system doesn't work as expected</p>
</li>
<li><p>Dig into the code, read it, and find the wrong logic</p>
</li>
<li><p>If the code looks right, maybe it has changed during the last 3 months but gets reverted. Let's check the git logs.</p>
</li>
<li><p>Find the ticket related to the code changes to find out what we expected at that time</p>
</li>
<li><p>Find the Slack message around that day to discover what is the decision making that didn't write to the ticket properly.</p>
</li>
</ol>
<h2 id="heading-framework-to-implement-a-new-feature">Framework to Implement a New Feature</h2>
<ol>
<li><p>Understand the requirement, and read the problem statement at least twice to ensure the reported issue is correct</p>
</li>
<li><p>Define the acceptance criteria</p>
</li>
<li><p>Look at the design (if any) twice, and notice all the details missing in the requirement</p>
</li>
<li><p>Ask the ticket creator all the questions to clarify the requirement until you make sure all the detail is covered</p>
</li>
<li><p>Now work on the code base, and the system design to find the solution</p>
</li>
<li><p>Write a solution for preview, maybe a list of bullet points, a diagram, or a document on how you resolve the problem</p>
</li>
<li><p>Consult the team members, get an agreement on the proposed solution</p>
</li>
<li><p>Go to coding, write the test first if your team follows TDD</p>
</li>
<li><p>Submit PR, ask your team for review</p>
</li>
<li><p>Resolve feedback, the rest is your normal deployment process...</p>
</li>
</ol>
<h2 id="heading-lets-start">Let’s start</h2>
<p>Implementing problem-solving frameworks isn't just about following a checklist – it's about fostering a culture of shared knowledge and continuous improvement.</p>
<p>Picture this: a team where every member, regardless of experience level, can confidently tackle complex issues. A workplace where knowledge isn't siloed but flows freely, captured in living, breathing frameworks that evolve with each challenge overcome.</p>
<p>This isn't just a pipe dream – it's a reality we can build, one framework at a time.</p>
<p>Here's my challenge to you:</p>
<ol>
<li><p>Start small: Pick one recurring problem in your team. It could be bug investigation, feature implementation, or even code review.</p>
</li>
<li><p>Document your process: Next time you tackle this problem, jot down each step you take. Be specific – what worked? What didn't?</p>
</li>
<li><p>Refine and share: Discuss your nascent framework with your team. Gather their insights, refine the steps, and create a shared resource.</p>
</li>
<li><p>Iterate and improve: As your team uses the framework, encourage feedback. Let it evolve based on real-world application.</p>
</li>
</ol>
<p>Remember, the goal isn't perfection – it's progress. By taking these steps, you're not just solving today's problems; you're building a foundation for tackling tomorrow's challenges.</p>
<p>I'd love to hear about your experiences. Have you used problem-solving frameworks in your work? What hurdles did you face? What unexpected benefits did you discover? Drop your thoughts in the comments below – let's learn from each other and elevate our craft together.</p>
]]></content:encoded></item><item><title><![CDATA[Design (and code) a job scheduling system]]></title><description><![CDATA[User story (simple)
As a user, I want to set up a time delay for my actions (e.g., send email) in a certain period, a certain day of the week, or a specific day of the year. So that my action can be executed with timing accuracy.
Function requirement...]]></description><link>https://nguyengineer.dev/design-and-code-a-job-scheduling-system</link><guid isPermaLink="true">https://nguyengineer.dev/design-and-code-a-job-scheduling-system</guid><category><![CDATA[scheduling]]></category><category><![CDATA[Go Language]]></category><category><![CDATA[PostgreSQL]]></category><category><![CDATA[System Design]]></category><category><![CDATA[distributed system]]></category><dc:creator><![CDATA[Nguyen Engineer]]></dc:creator><pubDate>Thu, 04 Jan 2024 12:06:58 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/laJW5pp-6Yw/upload/54f6eabf2a5386614e4981eb21505534.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-user-story-simple">User story (simple)</h1>
<p>As a user, I want to set up a time delay for my actions (e.g., send email) in a certain period, a certain day of the week, or a specific day of the year. So that my action can be executed with timing accuracy.</p>
<h2 id="heading-function-requirements">Function requirements</h2>
<p>The requirement isn’t strict to any detailed use of the scheduling system. Then I map some concepts into an online marketing system, in which users need to set up a marketing campaign with steps of action running during a period.</p>
<ul>
<li><p>[x] Ensure at least one delivery of every single job</p>
<ul>
<li>[x] Extra: prevent duplicate sends</li>
</ul>
</li>
<li><p>[x] The scheduler should be horizontally scalable to handle ~5m timers at peak</p>
</li>
<li><p>[x] The user can change the value or cancel timers on the fly</p>
</li>
<li><p>[x] Fairness, priority some group of users</p>
<ol>
<li><p>New signed-up users</p>
</li>
<li><p>Tenant with a small audience list.</p>
</li>
<li><p>The rest of the system, but shared the workload between them</p>
</li>
</ol>
</li>
<li><p>[x] Monitoring metrics <a target="_blank" href="https://www.notion.so/Monitoring-system-4e1f58613ab3476f8ae878b04ccac00c?pvs=21">Monitoring system</a></p>
<ul>
<li><p>[x] Time delay reports end-to-end processing time, from the time the job is due to the time it pulled out of the sending queue to process.</p>
</li>
<li><p>[x] Monitor the number of jobs processed per unit of time (e.g., per second)</p>
</li>
</ul>
</li>
<li><p>[x] TPS: 5m new timers/hour, and 5m due timers/hour</p>
</li>
<li><p>[x] The scheduler should have a p95 scheduling deviation below 10 seconds</p>
</li>
<li><p>[x] Cost. The worker didn’t design for scaling another worker internally. I would defer it to a dedicated service like K8S HPA, or self self-managed service that reads the metric and schedules new worker instance</p>
</li>
</ul>
<h2 id="heading-scope">Scope</h2>
<ol>
<li><p><strong>Backend Development</strong>: Focus on the backend, interacting with the system via APIs.</p>
</li>
<li><p><strong>Scheduling System Implementation</strong>:</p>
<ul>
<li><p>Design and implement a distributed job scheduling system without using/extending any existing Cloud or Open-source job scheduler.</p>
</li>
<li><p>Ensure the system can handle 10 million new and/or due timers per hour, with the potential to double this capacity.</p>
</li>
<li><p>Include a benchmark to demonstrate the system's capacity.</p>
</li>
</ul>
</li>
<li><p><strong>Exclusions</strong>:</p>
<ul>
<li>The implementation does not cover the actual job execution; the focus is on the scheduling aspect only. The jobs sent to the execution queue are done.</li>
</ul>
</li>
</ol>
<h1 id="heading-system-design">System design</h1>
<p><img src="https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F36f2141f-862d-4a6b-841d-8ce8a3da65cf%2Fc02c87e4-0540-4afd-b8b8-73c3016924f1%2FUntitled.png?table=block&amp;id=ad3b0491-40c4-4bde-9e38-2fd26d1fd741&amp;spaceId=36f2141f-862d-4a6b-841d-8ce8a3da65cf&amp;width=2000&amp;userId=4c4612be-099f-4160-992e-ab12600ad036&amp;cache=v2" alt class="image--center mx-auto" /></p>
<h2 id="heading-core-components">Core components</h2>
<ol>
<li><p><strong>API server</strong>: handle the user requests to schedule the job, handle the job delay time calculation</p>
</li>
<li><p><strong>PostgreSQL database</strong>: central database, that takes the key role in data consistency.</p>
</li>
<li><p><strong>Job processor</strong>: the due job checker, the heart of the system. Interval, concurrently check for the due job and send to queue</p>
</li>
<li><p><strong>Due job fixer</strong>: as the nature of a distributed system the process may stuck due to network errors, or code bugs. The job fixer helps to ensure no job is left behind.</p>
</li>
<li><p><strong>Data feeder</strong>: Feed the data to the system for demo purposes only</p>
</li>
<li><p><strong>Monitoring service</strong>. Use an agent to collect metric data for monitoring from the service and workers</p>
</li>
</ol>
<h2 id="heading-the-erd">The ERD</h2>
<p><img src="https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F36f2141f-862d-4a6b-841d-8ce8a3da65cf%2F04b483c7-1d3d-4f90-9a54-08c916c53411%2FUntitled.png?table=block&amp;id=3c3fbbb0-9aa0-43d1-abfa-12559e97ad61&amp;spaceId=36f2141f-862d-4a6b-841d-8ce8a3da65cf&amp;width=2000&amp;userId=4c4612be-099f-4160-992e-ab12600ad036&amp;cache=v2" alt /></p>
<h2 id="heading-entities">Entities</h2>
<p>I only describe the entities involved in the scope of this project</p>
<ul>
<li><p><strong>Tenant</strong>: to get the priority information</p>
</li>
<li><p><strong>Sequence</strong>: The holder of the template for the jobs, including steps and subscribers</p>
</li>
<li><p><strong>Step</strong>: The detailed information to build a job, could be a wait step or a job step</p>
</li>
<li><p><strong>Subscriber</strong>: The subscriber subscribes to the sequence. In this scope, we simply refer to it as a number to count how many jobs we schedule for subscribers of sequence.</p>
</li>
<li><p><strong>Job</strong>: The base unit of the scheduler system, the most important information is status and due time</p>
</li>
</ul>
<p>For the simplicity of the demo, I only define the table for the Job entity</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> <span class="hljs-keyword">IF</span> <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">EXISTS</span> public.jobs
(
    <span class="hljs-keyword">id</span>        <span class="hljs-built_in">serial</span>
        <span class="hljs-keyword">CONSTRAINT</span> jobs_pk
            PRIMARY <span class="hljs-keyword">KEY</span>,
    due_at    <span class="hljs-built_in">timestamp</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-keyword">NOW</span>() <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
    <span class="hljs-keyword">priority</span>  <span class="hljs-built_in">integer</span>   <span class="hljs-keyword">DEFAULT</span> <span class="hljs-number">0</span>,
    tenant_id <span class="hljs-built_in">integer</span>   <span class="hljs-keyword">DEFAULT</span> <span class="hljs-number">1</span>,
    <span class="hljs-keyword">status</span>    <span class="hljs-built_in">integer</span>   <span class="hljs-keyword">DEFAULT</span> <span class="hljs-number">0</span>,
    metadata  <span class="hljs-built_in">varchar</span>(<span class="hljs-number">100</span>)
);

<span class="hljs-keyword">ALTER</span> <span class="hljs-keyword">TABLE</span> public.jobs
    OWNER <span class="hljs-keyword">TO</span> postgres;

<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">INDEX</span> <span class="hljs-keyword">IF</span> <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">EXISTS</span> jobs_due_at_index
    <span class="hljs-keyword">ON</span> public.jobs (due_at);

<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">INDEX</span> <span class="hljs-keyword">IF</span> <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">EXISTS</span> jobs_priority_index
    <span class="hljs-keyword">ON</span> public.jobs (<span class="hljs-keyword">priority</span>);

<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">INDEX</span> <span class="hljs-keyword">IF</span> <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">EXISTS</span> jobs_status_index
    <span class="hljs-keyword">ON</span> public.jobs (<span class="hljs-keyword">status</span>);
</code></pre>
<h2 id="heading-workflows">Workflows</h2>
<p>Users send a POST request to the API server and the system will auto-start the rest of the workflow. Below are the key workflows</p>
<ol>
<li><strong>Schedule job process</strong></li>
</ol>
<p><img src="https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F36f2141f-862d-4a6b-841d-8ce8a3da65cf%2F525a79ae-dca4-427b-905d-9e8f9a4d4a42%2FUntitled.png?table=block&amp;id=433a4810-bc64-4d5b-9509-cfc823626293&amp;spaceId=36f2141f-862d-4a6b-841d-8ce8a3da65cf&amp;width=2000&amp;userId=4c4612be-099f-4160-992e-ab12600ad036&amp;cache=v2" alt /></p>
<p>Example request</p>
<pre><code class="lang-json">POST &lt;http:<span class="hljs-comment">//localhost:8081/schedule-job&gt;</span>
Content-Type: application/json

{
  <span class="hljs-attr">"type"</span>: <span class="hljs-string">"sequence"</span>,
  <span class="hljs-attr">"steps"</span>: [
    {
      <span class="hljs-attr">"type"</span>: <span class="hljs-string">"wait_certain_period"</span>,
      <span class="hljs-attr">"delay_period"</span>: <span class="hljs-number">1</span>,
      <span class="hljs-attr">"delay_unit"</span>: <span class="hljs-string">"minute"</span>
    },
    {
      <span class="hljs-attr">"type"</span>: <span class="hljs-string">"job"</span>,
      <span class="hljs-attr">"metadata"</span>: <span class="hljs-string">"{ 'any': 'thing' }"</span>
    },
    {
      <span class="hljs-attr">"type"</span>: <span class="hljs-string">"wait_weekday"</span>,
      <span class="hljs-attr">"weekdays"</span>: [
        <span class="hljs-string">"monday"</span>,
        <span class="hljs-string">"tuesday"</span>,
        <span class="hljs-string">"wednesday"</span>,
        <span class="hljs-string">"thursday"</span>,
        <span class="hljs-string">"friday"</span>
      ]
    },
    {
      <span class="hljs-attr">"type"</span>: <span class="hljs-string">"job"</span>,
      <span class="hljs-attr">"metadata"</span>: <span class="hljs-string">"job 2"</span>
    },
    {
      <span class="hljs-attr">"type"</span>: <span class="hljs-string">"wait_specific_date"</span>,
      <span class="hljs-attr">"date"</span>: <span class="hljs-string">"2023-12-29T18:48:34.200Z"</span>
    },
    {
      <span class="hljs-attr">"type"</span>: <span class="hljs-string">"job"</span>,
      <span class="hljs-attr">"metadata"</span>: <span class="hljs-string">"job 3"</span>
    }
  ],
  <span class="hljs-attr">"subscribers"</span>: <span class="hljs-number">20</span>
}
</code></pre>
<p>To satisfy the design requirement. The schedule-job API accepts a sequence of steps, if the step is a wait step, we will calculate the relative due time, this due time will be the start of the next step in the step list.</p>
<p>No matter where the job step is in the step list, once we get into the calculation of the due time for the job, we only need to use the due time calculated by the previous step as the reference.</p>
<p>Refer to this unit test to understand the expected input and output</p>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">TestCalculateNextJobs</span><span class="hljs-params">(t *testing.T)</span></span> {
    <span class="hljs-comment">// Define a start time for the sequence</span>
    startedAt := time.Date(<span class="hljs-number">2023</span>, <span class="hljs-number">12</span>, <span class="hljs-number">28</span>, <span class="hljs-number">12</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, time.UTC)

    <span class="hljs-comment">// Setup Sequence with steps</span>
    sequence := entity.Sequence{
        Steps: []entity.Step{
            &amp;entity.StepWaitCertainPeriod{DelayPeriod: <span class="hljs-number">1</span>, DelayUnit: entity.DelayUnitMinute},
            &amp;entity.StepJob{Metadata: <span class="hljs-string">"{ 'any': 'thing' }"</span>},
            &amp;entity.StepWaitWeekDay{WeekDays: []entity.WeekDay{entity.Monday, entity.Tuesday, entity.Wednesday, entity.Friday}},
            &amp;entity.StepJob{Metadata: <span class="hljs-string">"job 2"</span>},
            &amp;entity.StepWaitSpecificDate{Date: <span class="hljs-string">"2023-12-29T18:48:34.200Z"</span>},
            &amp;entity.StepJob{Metadata: <span class="hljs-string">"job 3"</span>},
        },
        Subscribers: <span class="hljs-number">2</span>,
    }

    <span class="hljs-comment">// Expected due dates for jobs</span>
    expectedDates := []time.Time{
        startedAt.Add(<span class="hljs-number">1</span> * time.Minute),                           <span class="hljs-comment">// 1 minute from startedAt (Job 1)</span>
        time.Date(<span class="hljs-number">2023</span>, <span class="hljs-number">12</span>, <span class="hljs-number">29</span>, <span class="hljs-number">12</span>, <span class="hljs-number">1</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, time.UTC),           <span class="hljs-comment">// Next weekday (Friday) for Job 2</span>
        time.Date(<span class="hljs-number">2023</span>, <span class="hljs-number">12</span>, <span class="hljs-number">29</span>, <span class="hljs-number">18</span>, <span class="hljs-number">48</span>, <span class="hljs-number">34</span>, <span class="hljs-number">200000000</span>, time.UTC), <span class="hljs-comment">// Specific date for Job 3</span>
    }

    got, err := controllers.CalculateNextJobs(sequence, startedAt)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        t.Fatalf(<span class="hljs-string">"CalculateNextJobs() error = %v"</span>, err)
    }

    <span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(got) != <span class="hljs-built_in">len</span>(expectedDates) {
        t.Fatalf(<span class="hljs-string">"Expected %d jobs, got %d"</span>, <span class="hljs-built_in">len</span>(expectedDates), <span class="hljs-built_in">len</span>(got))
    }

    <span class="hljs-keyword">for</span> i, job := <span class="hljs-keyword">range</span> got {
        log.Print(i, job)
        <span class="hljs-keyword">if</span> !job.DueAt.Equal(expectedDates[i]) {
            t.Errorf(<span class="hljs-string">"Job %d due at %v, want %v"</span>, i, job.DueAt, expectedDates[i])
        }
    }
}
</code></pre>
<p>After the job calculation, it will be sent to the database for the bulk insert.</p>
<ol>
<li><strong>Due job checker</strong></li>
</ol>
<p><img src="https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F36f2141f-862d-4a6b-841d-8ce8a3da65cf%2F44ce17a0-8bca-4a89-a69e-d2b9951bebeb%2FUntitled.png?table=block&amp;id=eb5d0b1c-45d9-4186-99e8-ba6f7ebaca77&amp;spaceId=36f2141f-862d-4a6b-841d-8ce8a3da65cf&amp;width=2000&amp;userId=4c4612be-099f-4160-992e-ab12600ad036&amp;cache=v2" alt /></p>
<p>After the jobs are inserted into the database with the due date, the due-job-checker workers can pull it from the database.</p>
<p>I use Postgres advisory lock here to ensure only one worker can pull the job at the same time.</p>
<p>But I still use the <code>SELECT FOR UPDATE</code> here to ensure no other process updates my records while I select it.</p>
<pre><code class="lang-go">rows, err := conn.Query(<span class="hljs-string">`
              UPDATE jobs 
              SET status = $1
              WHERE id IN (
                  SELECT id FROM jobs 
                  WHERE due_at &lt;= NOW() AND status = $2
                  ORDER BY priority 
                  LIMIT $3
                  FOR UPDATE SKIP LOCKED
              )
              RETURNING id, due_at`</span>, entity.JobStatusInProgress, entity.JobStatusInitialized, dueJobBatchSize)
</code></pre>
<p>The lock is released early right after I update its status to processing, this will help to increase the throughput of the system while other incoming actions don’t rely on the database.</p>
<ol>
<li><strong>Due job fixer</strong></li>
</ol>
<p><img src="https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F36f2141f-862d-4a6b-841d-8ce8a3da65cf%2Feeda3483-df43-43b2-a234-ed65c570ddca%2FUntitled.png?table=block&amp;id=c8ad9ad8-5e35-41da-9b0e-82db3e414ef6&amp;spaceId=36f2141f-862d-4a6b-841d-8ce8a3da65cf&amp;width=2000&amp;userId=4c4612be-099f-4160-992e-ab12600ad036&amp;cache=v2" alt /></p>
<p>To keep the queue table tidy. We need to clean up it frequently. This worker will internally (15 seconds) archive the processed job, in a real system I would move it to the archived table, but in this demo I simply just delete it.</p>
<p>For some reason, if the job fails to process after the maximum processing time limit (let’s say 10s), it is considered a failed job and moved back to the queue.</p>
<p>It is out of scope so I don’t go further on this but in a real system, we need to check more criteria to decide if a job has failed and retry it, not just the exceeded processing time criteria.</p>
<h2 id="heading-monitoring-system">Monitoring system</h2>
<p>I use external tools for monitoring. Prometheus to collect the metrics and Grafana to plot the data. The tools defined in the docker-compose.yml file</p>
<p><img src="https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F36f2141f-862d-4a6b-841d-8ce8a3da65cf%2F2043c63d-8d91-489f-82e0-90dcf53cea15%2FUntitled.png?table=block&amp;id=9fe651bb-a33f-4434-8f8b-75e9224b68bb&amp;spaceId=36f2141f-862d-4a6b-841d-8ce8a3da65cf&amp;width=2000&amp;userId=4c4612be-099f-4160-992e-ab12600ad036&amp;cache=v2" alt /></p>
<p>The general idea is to use the Prometheus SDK to collect the metric and send it to the Prometheus push gateway. Then set the Grafana to select the data source.</p>
<p>The metrics I collect</p>
<ul>
<li><p><strong>Job process TPS</strong>. This is the number of jobs this system can process per second. As you can see in the screenshot below, the TPS average is ~20.000 jobs/second.</p>
<p>  According to the requirement of 5M new timers + 5M due timers = 10M jobs/hours.</p>
<p>  It can be doubled at any time = 20M jobs/hours</p>
<p>  So 20.000 jobs/sec <em>60</em> 60 = 72.000.000 jobs/hour. This design can cover the system requirement more than 3 times.</p>
<p>  Note that this system runs on my local machine (MacBook M2 pro base edition), the Postgre database runs in docker with some resource constraints so the real system on the cloud can be much more capable of the higher load.</p>
<p>  <img src="https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F36f2141f-862d-4a6b-841d-8ce8a3da65cf%2F0214aea4-8372-49a6-8429-8177fa49e38c%2Ftps.png?table=block&amp;id=16678d0e-d84d-4f47-8b12-0bb546fb9820&amp;spaceId=36f2141f-862d-4a6b-841d-8ce8a3da65cf&amp;width=2000&amp;userId=4c4612be-099f-4160-992e-ab12600ad036&amp;cache=v2" alt /></p>
</li>
<li><p><strong>The p95 of the job due time delay</strong></p>
<p>  An average of 95% of job processing time is 1148 ms. Calculate from the exact UTC that the job due, to the time it is marked as processed.</p>
<p>  This metric is the delayed time from the time the user expected it to be sent to the time it is sent.</p>
<p>  <img src="https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F36f2141f-862d-4a6b-841d-8ce8a3da65cf%2F59d5bc77-1560-4575-af25-d1c67367152e%2FUntitled.png?table=block&amp;id=f025852a-65ca-4859-8c76-5eb5cb21e13c&amp;spaceId=36f2141f-862d-4a6b-841d-8ce8a3da65cf&amp;width=2000&amp;userId=4c4612be-099f-4160-992e-ab12600ad036&amp;cache=v2" alt /></p>
</li>
<li><p>Job in queue</p>
<p>  In my data feed, I set it up to schedule a job now and another job delayed 1 min later. As you can see the system is processing both new timers and due timers. As the data feed keeps running every second, this chart won’t be low unless I stop the data feed. But we can expect every job will be processed in about 1 second.</p>
<p>  <img src="https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F36f2141f-862d-4a6b-841d-8ce8a3da65cf%2Fc9e36c1b-240c-4b82-8c66-4cba31c9632b%2Fjobs_in_queue.png?table=block&amp;id=cd67c13f-977d-44bf-b33b-5c92950a3f10&amp;spaceId=36f2141f-862d-4a6b-841d-8ce8a3da65cf&amp;width=2000&amp;userId=4c4612be-099f-4160-992e-ab12600ad036&amp;cache=v2" alt /></p>
</li>
</ul>
<h1 id="heading-design-decision">Design decision</h1>
<ul>
<li><p>Why Go</p>
<ul>
<li><p>I need the robustness of the language and its ecosystem</p>
</li>
<li><p>Foolproof, not afraid of making code mistakes, many good practices and standards can be found online</p>
</li>
<li><p>Concurrency model.</p>
</li>
</ul>
</li>
<li><p>Why PostgreSQL</p>
<ul>
<li><p>Mostly a silver bullet for most projects that need robustness and flexibility. For example</p>
<ul>
<li><p>I utilized advisory lock, <code>select for update</code> skip locked for this project</p>
</li>
<li><p>In another project, I utilize the <code>jsonb</code> query and view.</p>
</li>
</ul>
</li>
<li><p>Rich documentation and resources</p>
</li>
</ul>
</li>
</ul>
<h2 id="heading-extending-ideas">Extending ideas</h2>
<ul>
<li><p>If I have more time on this project I will refactor it following the Clean Architecture structure to avoid logic fragmenting in many services workers. But for the scope of this project, the structured method works fine.</p>
</li>
<li><p>Add more integration tests and load tests for all the critical functions. I already have some tests but it is not enough to ensure the robustness of this system. Especially in the concurrent environment.</p>
</li>
</ul>
<h1 id="heading-read-me">Read me</h1>
<h3 id="heading-repository">Repository</h3>
<p><a target="_blank" href="https://github.com/finnng/job-scheduling-system">https://github.com/finnng/job-scheduling-system</a></p>
<h3 id="heading-prerequisites">Prerequisites</h3>
<ul>
<li><p>Go version 1.21.0</p>
</li>
<li><p>docker-compose (you may need to update docker-compose.yml for intel based computer)</p>
</li>
</ul>
<h3 id="heading-steps">Steps</h3>
<ol>
<li><p>Pull the source code</p>
</li>
<li><p>Start the databases: <code>docker compose up -d</code></p>
</li>
<li><p>Create a Postgre test database. Use any database client to create a database name <code>test</code> and grant permission for the default user <code>postgre</code> on it.</p>
</li>
</ol>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">DATABASE</span> <span class="hljs-keyword">test</span>;
<span class="hljs-keyword">GRANT</span> <span class="hljs-keyword">ALL</span> <span class="hljs-keyword">PRIVILEGES</span> <span class="hljs-keyword">ON</span> <span class="hljs-keyword">DATABASE</span> <span class="hljs-keyword">test</span> <span class="hljs-keyword">TO</span> postgre;
</code></pre>
<ol>
<li>Start the API server, it should automatically provision the tables. From the repo’s root directory, type</li>
</ol>
<pre><code class="lang-bash">go run api-server/app.go
</code></pre>
<ol>
<li>Start other workers to complete the full system, open other terminal tabs for these commands</li>
</ol>
<pre><code class="lang-bash">go run worker-due-job-checker/app.go
</code></pre>
<pre><code class="lang-bash">go run worker-job-fixer/app.go
</code></pre>
<pre><code class="lang-bash">go run data-feed/app.go
</code></pre>
<p>The data-feed worker will randomize the test data and send it to the API server to keep the system busy for the demo purpose. You can edit the test request to test all the cases of the scheduling scenario.</p>
<pre><code class="lang-go">payload := Payload{
            Type: <span class="hljs-string">"sequence"</span>,
            Steps: []Step{
                {
                    Type:     <span class="hljs-string">"job"</span>,
                    Metadata: <span class="hljs-string">"{ 'any': 'thing 1' }"</span>,
                },
                {
                    Type:        <span class="hljs-string">"wait_certain_period"</span>,
                    DelayPeriod: <span class="hljs-number">1</span>,
                    DelayUnit:   <span class="hljs-string">"minute"</span>,
                },
                {
                    Type:     <span class="hljs-string">"job"</span>,
                    Metadata: <span class="hljs-string">"{ 'any': 'thing 2' }"</span>,
                },
            },
            Subscribers: rand.Intn(<span class="hljs-number">10000</span>) + <span class="hljs-number">1</span>, <span class="hljs-comment">// Random number between 1 and 1000</span>
        }
</code></pre>
<p>Your terminal panels should look like this.</p>
<p><img src="https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F36f2141f-862d-4a6b-841d-8ce8a3da65cf%2Fe9420eb4-0520-4581-b340-fb12d789394f%2FUntitled.png?table=block&amp;id=5fcf772e-3d3c-462e-9b9f-e1e69a4e5009&amp;spaceId=36f2141f-862d-4a6b-841d-8ce8a3da65cf&amp;width=2000&amp;userId=4c4612be-099f-4160-992e-ab12600ad036&amp;cache=v2" alt /></p>
<h3 id="heading-monitoring">Monitoring</h3>
<p>I haven’t handled the Grafana database migration yet, so you need to head to the Grafana dashboard at <a target="_blank" href="https://www.notion.so/Monitoring-system-4e1f58613ab3476f8ae878b04ccac00c?pvs=21"><code>http://localhost:3000</code></a> according to the docker-compose file Grafana port.</p>
<ol>
<li><p>Setup Prometheus as the data source</p>
</li>
<li><p>Play around with the metrics sent from the scheduling system</p>
</li>
</ol>
<p><img src="https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F36f2141f-862d-4a6b-841d-8ce8a3da65cf%2F5d685a22-c0bd-4828-adf9-20cb86b8323c%2FUntitled.png?table=block&amp;id=856caad0-da74-408b-8987-4d6930ebc1e9&amp;spaceId=36f2141f-862d-4a6b-841d-8ce8a3da65cf&amp;width=2000&amp;userId=4c4612be-099f-4160-992e-ab12600ad036&amp;cache=v2" alt /></p>
<p>The complete dashboard should look like</p>
<p><img src="https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F36f2141f-862d-4a6b-841d-8ce8a3da65cf%2F56d91f73-f935-4ce8-9819-427bc9e8b1a2%2FUntitled.png?table=block&amp;id=88fbbc56-9497-4a23-8b80-fa18c0f8fa20&amp;spaceId=36f2141f-862d-4a6b-841d-8ce8a3da65cf&amp;width=2000&amp;userId=4c4612be-099f-4160-992e-ab12600ad036&amp;cache=v2" alt /></p>
]]></content:encoded></item><item><title><![CDATA[How I emerging AI into daily life (2023)]]></title><description><![CDATA[I'm using the latest commercial AI technologies, including Copilot for business, Copilot chat, Grammarly, Notion AI, ChatGPT Plus, and maybe other AI features embedded in a product that I am not aware]]></description><link>https://nguyengineer.dev/how-i-emerging-ai-into-daily-life-2023</link><guid isPermaLink="true">https://nguyengineer.dev/how-i-emerging-ai-into-daily-life-2023</guid><category><![CDATA[#ai-tools]]></category><category><![CDATA[chatgptplus]]></category><category><![CDATA[documentation]]></category><category><![CDATA[virtual assistant]]></category><category><![CDATA[AI]]></category><dc:creator><![CDATA[Nguyen Engineer]]></dc:creator><pubDate>Sat, 02 Dec 2023 04:51:26 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1701491907853/478727b8-9f26-4d71-9229-def4ccf62a4a.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I'm using the latest commercial AI technologies, including Copilot for business, Copilot chat, Grammarly, Notion AI, ChatGPT Plus, and maybe other AI features embedded in a product that I am not aware of.</p>
<p>I took a basic course on how to create an AI algorithm, it is a Gradient Descent algorithm and it is so basic. I didn't go further with the more advanced algorithms because it is not my expertise area, but it gave me a big picture of how AI works. Prove me wrong. AI now is all about statistics on the highest possible output based on the input.</p>
<p>As long as AI is still based on the training data and the probability, it still needs the input to produce output, it won't be able to destroy any of humankind, and it can't "think" on its own. So what can it do?</p>
<p>For me, I use AI for those purposes</p>
<ol>
<li><p>Coding</p>
<ol>
<li><p>Code suggestion</p>
</li>
<li><p>Code fixing</p>
</li>
<li><p>Chat with code.</p>
</li>
<li><p>Chat with code base (not yet, still waiting for GitHub Enterprise to release this feature - this is freaking cutting edge)</p>
</li>
</ol>
</li>
<li><p>Research</p>
<ol>
<li><p>Anything, from the new plant I found on the road to the software design philosophy or rocket science</p>
</li>
<li><p>Product research</p>
</li>
</ol>
</li>
<li><p>Documentation</p>
<ol>
<li><p>Fix grammar</p>
</li>
<li><p>Find the missing argument in my document</p>
</li>
</ol>
</li>
<li><p>Helping my wife with her work</p>
<ol>
<li><p>Refine the requirement</p>
</li>
<li><p>Draw the diagram</p>
</li>
</ol>
</li>
<li><p>Making AI-based product</p>
</li>
</ol>
<p>And many more purposes.</p>
<h2>Coding</h2>
<p>My company subscribes to the business Github Copilot, surely I have access to the IDE plugin for my Neovim. But I still need the chat, Copilot chat didn't have a plugin for Neovim yet, so I have to create my plugin, you know, by utilizing AI to generate the Lua code. I have another post here on a plugin I copilot with AI to create <a href="https://finnng.hashnode.dev/quickly-create-a-scatch-in-neovim">https://finnng.hashnode.dev/quickly-create-a-scatch-in-neovim</a></p>
<p>I'm working for a company that partners with other enterprises so data security will be the top concern in mind when using AI for enterprise work. All the code related to work needs to be under the API given by the company.</p>
<h3>Benefits</h3>
<ul>
<li><p><strong>I can focus on ideas, rather than coding.</strong> Because I know coding is easy when ideas are clear.</p>
</li>
<li><p><strong>Write code faster</strong>. Copilot is amazing, it can guess what are my next lines with high accuracy. It speeds me up a lot for the repetitive task and also reminds me of the syntax I don't remember.</p>
</li>
<li><p><strong>Faster experiment</strong>. I don't know Lua, I don't know Rust, or I don't have expertise in Python, but in case I need to modify a lib or a snippet of code I can always ask for AI to help.</p>
</li>
<li><p><strong>Faster onboarding.</strong> When the Copilot Chat With Code Base is released. I believe it will be the <em><strong>game changer</strong></em>. In history, we need the expert in the team to tell you where the place to change code to satisfy the new requirement. Now the new commer and all the developers can benefit from this. We still need humans to work on the code, consider the pros and cons of the solution, and think through the system and how the system affects the users - <em><strong>AI can't do that</strong></em>, but at least it can connect the dots to help you gain understanding faster. Especially for the huge code base in the enterprise company.</p>
</li>
</ul>
<h3>Drawbacks</h3>
<ul>
<li><p><strong>It makes me lazy</strong>. I need to be mindful when using AI. I need to keep in mind how it works, and how to judge its result, but sometimes I am still too lazy to do that and just use the output. This is a dangerous habit. Getting your hand dirty, and scratching your head is a part of the process, it helps to develop the brain. We shouldn't let AI take it from us. You can research this, or I can give a few</p>
<ul>
<li><p>Learning by doing helps students perform better in science</p>
</li>
<li><p><a href="https://news.harvard.edu/gazette/story/2021/10/study-finds-students-learn-better-through-physical-participation/#:~:text=Study%20finds%20students%20learn%20better,Siliezar%20Harvard%20Staff%20Writer%20Date">Finding hands-on approaches to remote learning</a></p>
</li>
</ul>
</li>
<li><p><strong>I changing my working habits.</strong> This one comes from laziness, and also many other drawbacks you can list. The solution is mindfulness when using tools.</p>
</li>
</ul>
<h2>Researching</h2>
<p>ChatGPT Plus has a crucial role in my research process. Google search is too noisy and not focused on the answer makes me imagine the day that Google becomes another Yahoo.</p>
<p>My research process also changed. ChatGPT Plus can access the internet in real time and it is a game changer. It is based on statistics so it can list out the top popular keywords, and terminologies on the thing I research.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1701489617042/5f0512f3-0027-4413-9c83-974ade8a0e26.png" alt="" style="display:block;margin:0 auto" />

<p>After I have the consolidated keywords, articles, and terminologies. I can ask ChatGPT for more details or I can do a Google search myself to prove the output.</p>
<p>Again, we need to be mindful of the results of AI, and do our research. Keep in mind that it is a copilot. Not a pilot.</p>
<h2>Documentation</h2>
<p>I'm using Notion AI at work, it is under an enterprise license for sensitive data protection. That's why I can only compose the work documentation with it.</p>
<p>I mostly use it for grammar fixing or sometimes review my docs and find the missing piece I didn't mention or unclear.</p>
<p>Once again, it isn't a "can't live with" tool, it helps me work faster but I can't count how many percent faster.</p>
<p>I found Grammarly to be much more useful as I'm not a native English speaker. Grammarly has a MacOS app that can help to fix Grammar everywhere. It has a clear agreement to not access user sensitive data but I'm not sure so I disabled it in Notion to not let it read the company-related document.</p>
<h2>Helping my wife with her work</h2>
<p>This is where all the AI comes to shine. My wife is not a developer, nor a technical person, so the output of AI should be natural to humans.</p>
<p>I often ask AI to draw the diagram via the PlantUML code and paste it into <a href="https://www.planttext.com/">https://www.planttext.com/</a> to view the diagram</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1701490678972/53c79e7d-5ca8-43ec-b15b-a96eb797d802.png" alt="" style="display:block;margin:0 auto" />

<p>ChatGPT Plus has the plugin to draw the diagram in the chat too, but it is a paid solution so I'm fine with the PlantUML.</p>
<p>ChatGPT Plus is so advanced at document refining as the GPT-4 model is training on text. I have no comment about this as the whole world is already impressed by it. But if you use the free account with GPT-3.5 it may not good as you expected.</p>
<p>I installed the app and it can take pictures as the input too. Now I can ask it what is this plant in the picture. After a few back-and-forth corrects it can show me the correct answer.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1701491440241/2c028757-0c40-4e65-8b93-7fa80b4d95c6.png" alt="" style="display:block;margin:0 auto" />

<h3>Voice chat with AI</h3>
<p>ChatGPT app also has a voice mode to let you chat with AI by voice. I usually use this in the morning walk while my mind still wandering on some research. It makes me feel like a scene in the movie when a man talking with his AI assistant like talking with people. Interesting, right? Now everyone can hire an assistant.</p>
<img src="https://149695847.v2.pressablecdn.com/wp-content/uploads/2016/10/her5.jpg" alt="Movie HER – a Portrayal of Future AI Capabilities" style="display:block;margin:0 auto" />

<h2>Making AI-based product</h2>
<p>I made a few small products, and editor plugins based on AI. But since it tightens with OpenAI and its core competencies are on OpenAI's shoulder, I never going to do business with it. You can read more about this opinion here <a href="https://finnng.hashnode.dev/dont-outsource-your-core-competencies">https://finnng.hashnode.dev/dont-outsource-your-core-competencies</a></p>
<p>I may share a few projects in the future if I have time.</p>
<h1>Conclusion</h1>
<p>That's it. Writing a blog is hard, please give it an upvote if you read it through.</p>
<p>I hope it gives you some insights into using AI to improve your daily life.</p>
]]></content:encoded></item><item><title><![CDATA[I'm surfing on the trend of HTMX]]></title><description><![CDATA[I'm tired of JS, not to mention TS and mostly I blame the whole JS ecosystem.
If I can come in time to tell one thing to my younger self I will tell him all in BTC back in 2012! Just kidding, I will tell him to choose Java or at least C# Asp.net. Don...]]></description><link>https://nguyengineer.dev/im-surfing-on-the-trend-of-htmx</link><guid isPermaLink="true">https://nguyengineer.dev/im-surfing-on-the-trend-of-htmx</guid><category><![CDATA[htmx]]></category><category><![CDATA[Go Language]]></category><dc:creator><![CDATA[Nguyen Engineer]]></dc:creator><pubDate>Sat, 11 Nov 2023 09:09:25 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/yRv2F4NN3BE/upload/78801738b15c7b39ff148f71cb3f425c.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I'm tired of JS, not to mention TS and mostly I blame the whole JS ecosystem.</p>
<p>If I can come in time to tell one thing to my younger self I will tell him all in BTC back in 2012! Just kidding, I will tell him to choose Java or at least C# Asp.net. Don't choose anything that trendy when you are still a novice, j/k again, who knows what the future holds? And we can't time travel.</p>
<p>JS is still my main tech stack on the backend, along with Go, and C#. But on frontend, I don't have a choice but JS. While the backend can change easily, the frontend is different, I'm not a frontend guy so I don't have a passion for refactoring, or rewriting frontend code regardless of whatever the technology it was.</p>
<p>I am also tired of Webpack, frontend build pipeline, and dependencies chain break because the author adopted something new to their repo. I want to stay with the stable version, but then Dependabot says I have some security risks if I stay with some specific version. What should I do but not to stay up-to-date with my dependencies?</p>
<p>It is time to experiment. But if I do serverside rendering I will go back to the day of PHP and the template engine, back to the AJAX and JQuery if I want to make the page not reload on every click. Then I found HTMX and these things make sense</p>
<blockquote>
<ul>
<li><p>Why should only <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a"><code>&lt;a&gt;</code></a> and <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form"><code>&lt;form&gt;</code></a> be able to make HTTP requests?</p>
</li>
<li><p>Why should only <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Element/click_event"><code>click</code></a> &amp; <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/submit_event"><code>submit</code></a> events trigger them?</p>
</li>
<li><p>Why should only <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/GET"><code>GET</code></a> &amp; <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST"><code>POST</code></a> methods be <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods">available</a>?</p>
</li>
<li><p>Why should you only be able to replace the <strong>entire</strong> screen?</p>
</li>
</ul>
<p>By removing these arbitrary constraints, htmx completes HTML as a <a target="_blank" href="https://en.wikipedia.org/wiki/Hypertext">hypertext</a></p>
<p>htmx is a library that allows you to access modern browser features directly from HTML, rather than using javascript.</p>
</blockquote>
<p>With HTMX I don't have to use JS anymore, so it requires the server to work together to produce updated HTML (no page reload). The communication between the backend and frontend now is HTML, not JSON, so the server is in charge of frontend logic.</p>
<p>I like this idea, based on my experience, putting the business logic on frontend is a mess, the frontend guy tended to work with frontend only, they tried some magic with the backend JSON response to satisfy the business logic and left the mess here. We asked each other many times, where is the bug, frontend or backend. Hence we can easily trace it, but it is still time-consuming.</p>
<p>We should let the frontend guy do what he does best: frontend, styling, animation. The backend guy does the computation, and data formatting.</p>
<p>Enough talking, in the next part, I will start writing an htmx application.</p>
]]></content:encoded></item><item><title><![CDATA[Quickly create a scatch in Neovim]]></title><description><![CDATA[I missed the feature of quickly creating a scratch when I switched back to Vim from Webstorm.

A scratch file is handy, it auto-saves somewhere on your computer and allows you to paste temporary content here to do whatever you want.
In Vim or Neovim ...]]></description><link>https://nguyengineer.dev/quickly-create-a-scatch-in-neovim</link><guid isPermaLink="true">https://nguyengineer.dev/quickly-create-a-scatch-in-neovim</guid><category><![CDATA[nvim, ]]></category><category><![CDATA[Lua]]></category><category><![CDATA[scratch]]></category><category><![CDATA[WebStorm]]></category><category><![CDATA[dotfiles]]></category><dc:creator><![CDATA[Nguyen Engineer]]></dc:creator><pubDate>Sat, 11 Nov 2023 08:18:02 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1699690666500/f0421036-25f6-40d2-81d6-370d7f54b7d0.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I missed the feature of quickly creating a scratch when I switched back to Vim from Webstorm.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1699689525829/d023e417-f141-43b6-8998-b65af2dc244c.png" alt class="image--center mx-auto" /></p>
<p>A scratch file is handy, it auto-saves somewhere on your computer and allows you to paste temporary content here to do whatever you want.</p>
<p>In Vim or Neovim you need to open a new buffer with <code>:new</code> or <code>:vnew</code> , then <code>:set filetype {sometype}</code>. Paste the content, then <code>:w</code> to save it somewhere, you also need to name it, what a cumbersome process.</p>
<p>Say no more, this is my Neovim Lua script.</p>
<pre><code class="lang-javascript">-- Require the fzf-lua plugin
local fzf_lua = <span class="hljs-built_in">require</span>(<span class="hljs-string">"fzf-lua"</span>)

-- <span class="hljs-built_in">Function</span> to open a <span class="hljs-keyword">new</span> scratch buffer <span class="hljs-keyword">with</span> a specific filetype
local <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">open_scratch_buffer</span>(<span class="hljs-params">filetype</span>)
    -- <span class="hljs-title">Generate</span> <span class="hljs-title">a</span> <span class="hljs-title">timestamp</span> <span class="hljs-title">for</span> <span class="hljs-title">unique</span> <span class="hljs-title">file</span> <span class="hljs-title">naming</span>
    <span class="hljs-title">local</span> <span class="hljs-title">date_time</span> = <span class="hljs-title">os</span>.<span class="hljs-title">date</span>(<span class="hljs-params"><span class="hljs-string">"%Y-%m-%d_%H-%M-%S"</span></span>)
    -- <span class="hljs-title">Define</span> <span class="hljs-title">the</span> <span class="hljs-title">directory</span> <span class="hljs-title">where</span> <span class="hljs-title">scratch</span> <span class="hljs-title">files</span> <span class="hljs-title">will</span> <span class="hljs-title">be</span> <span class="hljs-title">stored</span>
    <span class="hljs-title">local</span> <span class="hljs-title">scratch_dir</span> = <span class="hljs-title">vim</span>.<span class="hljs-title">fn</span>.<span class="hljs-title">expand</span>(<span class="hljs-params"><span class="hljs-string">"~/.vim/scratches/"</span></span>)
    -- <span class="hljs-title">Create</span> <span class="hljs-title">the</span> <span class="hljs-title">full</span> <span class="hljs-title">path</span> <span class="hljs-title">for</span> <span class="hljs-title">the</span> <span class="hljs-title">new</span> <span class="hljs-title">scratch</span> <span class="hljs-title">file</span>
    <span class="hljs-title">local</span> <span class="hljs-title">filename</span> = <span class="hljs-title">scratch_dir</span> .. <span class="hljs-title">date_time</span> .. "." .. <span class="hljs-title">filetype</span>

    -- <span class="hljs-title">Create</span> <span class="hljs-title">the</span> <span class="hljs-title">scratch</span> <span class="hljs-title">directory</span> <span class="hljs-title">if</span> <span class="hljs-title">it</span> <span class="hljs-title">does</span> <span class="hljs-title">not</span> <span class="hljs-title">exist</span>
    <span class="hljs-title">vim</span>.<span class="hljs-title">fn</span>.<span class="hljs-title">mkdir</span>(<span class="hljs-params">scratch_dir, <span class="hljs-string">"p"</span></span>)
    -- <span class="hljs-title">Open</span> <span class="hljs-title">a</span> <span class="hljs-title">new</span> <span class="hljs-title">vertical</span> <span class="hljs-title">split</span> <span class="hljs-title">with</span> <span class="hljs-title">the</span> <span class="hljs-title">created</span> <span class="hljs-title">file</span>
    <span class="hljs-title">vim</span>.<span class="hljs-title">cmd</span>(<span class="hljs-params"><span class="hljs-string">"vnew "</span> .. vim.fn.fnameescape(filename)</span>)
    -- <span class="hljs-title">Set</span> <span class="hljs-title">the</span> <span class="hljs-title">filetype</span> <span class="hljs-title">for</span> <span class="hljs-title">the</span> <span class="hljs-title">new</span> <span class="hljs-title">buffer</span> <span class="hljs-title">for</span> <span class="hljs-title">appropriate</span> <span class="hljs-title">syntax</span> <span class="hljs-title">highlighting</span>
    <span class="hljs-title">vim</span>.<span class="hljs-title">api</span>.<span class="hljs-title">nvim_buf_set_option</span>(<span class="hljs-params"><span class="hljs-number">0</span>, <span class="hljs-string">"filetype"</span>, filetype</span>)
    -- <span class="hljs-title">Save</span> <span class="hljs-title">the</span> <span class="hljs-title">file</span> <span class="hljs-title">to</span> <span class="hljs-title">create</span> <span class="hljs-title">it</span> <span class="hljs-title">on</span> <span class="hljs-title">disk</span>
    <span class="hljs-title">vim</span>.<span class="hljs-title">api</span>.<span class="hljs-title">nvim_command</span>(<span class="hljs-params"><span class="hljs-string">"write"</span></span>)
<span class="hljs-title">end</span>

-- <span class="hljs-title">Global</span> <span class="hljs-title">function</span> <span class="hljs-title">to</span> <span class="hljs-title">select</span> <span class="hljs-title">a</span> <span class="hljs-title">filetype</span> <span class="hljs-title">and</span> <span class="hljs-title">create</span> <span class="hljs-title">a</span> <span class="hljs-title">scratch</span> <span class="hljs-title">buffer</span>
<span class="hljs-title">function</span> <span class="hljs-title">_G</span>.<span class="hljs-title">select_filetype_and_create_scratch</span>(<span class="hljs-params"></span>)
    -- <span class="hljs-title">Retrieve</span> <span class="hljs-title">the</span> <span class="hljs-title">list</span> <span class="hljs-title">of</span> <span class="hljs-title">Vim</span> <span class="hljs-title">syntax</span> <span class="hljs-title">files</span> <span class="hljs-title">from</span> <span class="hljs-title">the</span> <span class="hljs-title">runtime</span> <span class="hljs-title">path</span>
    <span class="hljs-title">local</span> <span class="hljs-title">syntax_dir</span> = <span class="hljs-title">vim</span>.<span class="hljs-title">fn</span>.<span class="hljs-title">globpath</span>(<span class="hljs-params">vim.fn.getenv(<span class="hljs-string">"VIMRUNTIME"</span>), <span class="hljs-string">"syntax/*.vim"</span>, false, true</span>)
    <span class="hljs-title">local</span> <span class="hljs-title">filetypes</span> = </span>{}

    -- Extract filetype names <span class="hljs-keyword">from</span> the retrieved syntax file paths
    <span class="hljs-keyword">for</span> _, filepath <span class="hljs-keyword">in</span> ipairs(syntax_dir) <span class="hljs-keyword">do</span>
        local filetype = filepath:match(<span class="hljs-string">"syntax[/\\](.+).vim$"</span>)
        <span class="hljs-keyword">if</span> filetype then
            -- Add the filetype to the list
            table.insert(filetypes, filetype)
        end
    end

    -- Execute fzf <span class="hljs-keyword">with</span> the list <span class="hljs-keyword">of</span> filetypes
    fzf_lua.fzf_exec(filetypes, {
        prompt = <span class="hljs-string">"Filetypes&gt; "</span>,
        actions = {
            -- Action to perform when a filetype is selected
            [<span class="hljs-string">"default"</span>] = <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">selected</span>)
                <span class="hljs-title">open_scratch_buffer</span>(<span class="hljs-params">selected[<span class="hljs-number">1</span>]</span>)
            <span class="hljs-title">end</span>,
        },
    })
<span class="hljs-title">end</span>

-- <span class="hljs-title">Set</span> <span class="hljs-title">a</span> <span class="hljs-title">key</span> <span class="hljs-title">mapping</span> <span class="hljs-title">for</span> <span class="hljs-title">the</span> <span class="hljs-title">function</span>
-- &lt;<span class="hljs-title">leader</span>&gt;<span class="hljs-title">t</span> <span class="hljs-title">will</span> <span class="hljs-title">trigger</span> <span class="hljs-title">the</span> <span class="hljs-title">filetype</span> <span class="hljs-title">selection</span> <span class="hljs-title">and</span> <span class="hljs-title">scratch</span> <span class="hljs-title">buffer</span> <span class="hljs-title">creation</span>
<span class="hljs-title">vim</span>.<span class="hljs-title">api</span>.<span class="hljs-title">nvim_set_keymap</span>(<span class="hljs-params"><span class="hljs-string">"n"</span>, <span class="hljs-string">"&lt;leader&gt;t"</span>, <span class="hljs-string">":lua _G.select_filetype_and_create_scratch()&lt;CR&gt;"</span>, { noremap = true }</span>)</span>
</code></pre>
<p>What does it do?</p>
<ol>
<li><p>Get the list of vim supported file types, grep the base name only to make the list of available file types</p>
</li>
<li><p>Use fzf.lua API <code>fzf_exec</code> to serve the list, and provide you with a UI for fuzzy searching</p>
</li>
<li><p>Select the user's selected file type, open a new buffer to the right, name it the current datetime, and save it. Now you can paste your content here.</p>
</li>
</ol>
<p>Why the auto-save part is important?</p>
<p>Because some commands like <code>:EslintAutoFix</code> won't run on an unsaved buffer.</p>
<p>To use this, you need:</p>
<ol>
<li><p>You Neovim are using init.lua</p>
</li>
<li><p>You need to install fzf-lua. I tried with fzf.vim but no luck, incompatible.</p>
</li>
<li><p>Add this script to your init.lua file <code>require("scratch_config")</code> assumed you have this script name <code>scratch_config</code></p>
</li>
<li><p>Benefit</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1699690132946/28e39378-5ed7-4641-8406-676e8fc330dd.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1699690402002/1d4b2446-ad8d-47ae-8af8-e3f5f0065bc4.png" alt class="image--center mx-auto" /></p>
<p>For more information, check my latest update <a target="_blank" href="https://github.com/finnng/dotfiles/blob/master/nvim/lua/scratch_config.lua">https://github.com/finnng/dotfiles/blob/master/nvim/lua/scratch_config.lua</a></p>
<p>This is my repo so it may change at any time, in that case you can visit the commit <a target="_blank" href="https://github.com/finnng/dotfiles/commit/c09b79cd455e940cdb07ad836bfce2ca255fd2de">https://github.com/finnng/dotfiles/commit/c09b79cd455e940cdb07ad836bfce2ca255fd2de</a></p>
]]></content:encoded></item><item><title><![CDATA[Finally, I found the best way to keep dot files in sync]]></title><description><![CDATA[I was struggling to keep my dotfiles in GitHub in sync with the dot files in the $HOME directory. But now it is so smooth.
Cut to the chase, here is what I do:

The $HOME folder will be the single source of truth. I keep all the original files here

...]]></description><link>https://nguyengineer.dev/finally-i-found-the-best-way-to-keep-dot-files-in-sync</link><guid isPermaLink="true">https://nguyengineer.dev/finally-i-found-the-best-way-to-keep-dot-files-in-sync</guid><category><![CDATA[dotfiles]]></category><category><![CDATA[dotfile]]></category><category><![CDATA[vim]]></category><category><![CDATA[neovim]]></category><category><![CDATA[karabiner-elements]]></category><dc:creator><![CDATA[Nguyen Engineer]]></dc:creator><pubDate>Wed, 08 Nov 2023 14:31:45 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/nAYC7M-7M00/upload/d776d84dfda95c80ed3f058665952285.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I was struggling to keep my dotfiles in GitHub in sync with the dot files in the $HOME directory. But now it is so smooth.</p>
<p>Cut to the chase, here is what I do:</p>
<ol>
<li><p>The $HOME folder will be the <strong>single source of truth.</strong> I keep all the original files here</p>
</li>
<li><p>Use rsync to sync all the files from $HOME to dotfiles Github local repo</p>
</li>
<li><p>Commit the files to the origin to backup.</p>
</li>
</ol>
<p>You may have many files so you may have many rsync commands, let pack it into a bash function and add it to your ~/.zshrc</p>
<pre><code class="lang-bash"><span class="hljs-function"><span class="hljs-title">dotsync</span></span>() {
  rsync -r ~/.config/nvim/* ~/projects/dotfiles/nvim
  rsync -r ~/.config/karabiner/* ~/projects/dotfiles/karabiner
  rsync -r ~/.config/kitty/* ~/projects/dotfiles/kitty
}
</code></pre>
<p>My dotfiles repo is public here <a target="_blank" href="https://github.com/finnng/dotfiles">https://github.com/finnng/dotfiles</a></p>
<p>There are a lot of benefits to this approach</p>
<ol>
<li><p>My dotfiles repo is pristine since the file itself is the single source of truth</p>
</li>
<li><p>No more linking issues like sometimes you have to force linking with <code>ln -fs</code></p>
</li>
<li><p>Stress-free, no more file conflict, let the rsync command deal with it.</p>
</li>
</ol>
]]></content:encoded></item><item><title><![CDATA[Don't outsource your core competencies]]></title><description><![CDATA[I have scheduled myself to write a weekly blog, so I am now thinking about what I want to share this week. There's always so much to talk about when it comes to AI and its impact on businesses.
This w]]></description><link>https://nguyengineer.dev/dont-outsource-your-core-competencies</link><guid isPermaLink="true">https://nguyengineer.dev/dont-outsource-your-core-competencies</guid><category><![CDATA[generative ai]]></category><category><![CDATA[SMEs]]></category><category><![CDATA[Product Design]]></category><category><![CDATA[product development]]></category><dc:creator><![CDATA[Nguyen Engineer]]></dc:creator><pubDate>Sat, 21 Oct 2023 03:31:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/64479ed989b09f5069ab7a18/0969cada-5193-49e1-bc68-7b98e3a92c65.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I have scheduled myself to write a weekly blog, so I am now thinking about what I want to share this week. There's always so much to talk about when it comes to AI and its impact on businesses.</p>
<p>This week, I just finished a task related to AI. It was nothing special, mostly just calling another third-party service to outsource some work. But even in this seemingly mundane task, there were some interesting insights that I would like to share with you.</p>
<p>Small AI businesses nowadays mostly rely on generative AI to process their core business operations. It's an efficient approach, but it also comes with its fair share of risks. Just yesterday, I observed that the OpenAI service was down, which had a direct impact on a feature in our product. Until the service was back up and running, that feature simply didn't work. Thankfully, it was just a small feature with a lot of fault tolerance built in. But it got me thinking, what if an AI business heavily relied on the OpenAI service? They would go down together, potentially causing significant disruptions to their operations.</p>
<p>This situation made me realize the importance of not outsourcing our core competence. It's easy for anyone to build an application around OpenAI without truly understanding how AI works. However, there's a better way. By using an open-source model, we can take control of our AI implementation. We can run it on our own infrastructure, gaining a deeper understanding of its inner workings. We can train and fine-tune the model to better suit our needs, all while ensuring the security and privacy of our customer data. This approach empowers us to be the true owners of our core competency, allowing us to navigate the ever-changing AI landscape with confidence.</p>
<p>In our company, we take a practical approach to AI. While we use AI to improve certain features and content of our product, our main focus is still on the traditional market. Our product is already leading the market, even without AI. However, AI allows us to enhance our offerings and provide more value to our customers. It's important to note that our success in the market is not solely dependent on AI. We have a team of industry experts who drive our success.</p>
<p>Reflecting on my recent experiences with AI and its role in our business, I appreciate the need for a balanced approach. While AI brings great benefits, we must also be aware of the potential risks and maintain our core competencies. By doing so, we can leverage the power of AI to propel our businesses forward while maintaining control.</p>
]]></content:encoded></item><item><title><![CDATA[An underrated problem solving skill]]></title><description><![CDATA[It is the ability to transform the verbal, rough description into programmable tasks. And this skill is hard to test out during the interview.
Normally we - software engineers go to an interview, and they throw a competitive programming problem to us...]]></description><link>https://nguyengineer.dev/an-underrated-problem-solving-skill</link><guid isPermaLink="true">https://nguyengineer.dev/an-underrated-problem-solving-skill</guid><category><![CDATA[problem solving skills]]></category><category><![CDATA[pragmatic]]></category><category><![CDATA[Software Engineering]]></category><category><![CDATA[engineering-management]]></category><dc:creator><![CDATA[Nguyen Engineer]]></dc:creator><pubDate>Sat, 14 Oct 2023 04:28:35 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/mpN7xjKQ_Ns/upload/f1be945a3ffc8009920150aa8e2f48d9.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>It is the ability to transform the verbal, rough description into programmable tasks. And this skill is hard to test out during the interview.</p>
<p>Normally we - software engineers go to an interview, and they throw a competitive programming problem to us and say, solve it or **** off. They say that is the best way for engineers to show their problem-solving skills. But I found it isn't true. Here we go, I'm not a big influencer so it won't hurt to keep reading this perspective.</p>
<p>Some of my coworkers are good at solving LeetCode problems, and they are good at requirement problem-solving too. But the underlying aspect of this is some of them cannot still</p>
<ul>
<li><p>Explain the technical understanding to the stakeholders in their language.</p>
</li>
<li><p>Thinking through the entire system on how this new change affects the other parts and the consequences.</p>
</li>
<li><p>Well-written and verbal English. I'm not talking about the fluency in English here, I worked with the native UK and US coworker, it's their native language but their presentation was not always as clear as I expected. This skill includes the ability to draw diagrams, write the docs, do presentations, and lead meetings.</p>
</li>
<li><p>Communication and collaboration, delegation, listening skills, low ego, ease to work with, to teach the other in the day-to-day work.</p>
</li>
</ul>
<p>LeetCode problem, somehow is on the other side of the pragmatic problem-solving skills above. I don't understand why the company tests mathematic skills but doesn't use any of those skills in real work. What they need is a person who can transform the product owner thinking into a running feature, while maintaining the harmonic atmosphere in the team. Live long and prosper.</p>
<p>People with mathematic skill is good for their product at some point, I worked with some 10x engineers, and they rock, but they also burned the whole team to the ground lately with their technical-centric thinking, high IQ doesn't always come with high EQ. Sadly I usually find myself in a pro-IQ interview where competitive programming is not my expertise, some time I pass the interview but I have to work with those 10x engineers, that was a terrible and stressful experience.</p>
<p>Finally, with 10 years of experience in this industry, I can say that people with my kind of problem-solving skills still have a place, with high pay and the open road for higher positions, but the company that respects those skills is not popular.</p>
<p>Pieces of Advice? Here is my 2 cents</p>
<p>For engineers, just keep moving, you know you are the unpopular group, and there are always the right places for you.</p>
<p>For companies, let's say EQ and IQ ranged from 0 to 10. You better look for a person who has both IQ and EQ above 5, that's enough. Don't just look at the IQ 10 and ignore the EQ aspect, the low EQ person is a destroyer. Mathematics problem-solving is not everything, you need engineers with pragmatic skills too. The perfect person may still exist if you keep looking, maybe some day you can find them.</p>
]]></content:encoded></item></channel></rss>