<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Systems on vnykmshr</title><link>https://blog.vnykmshr.com/writing/categories/systems/</link><description>Recent content in Systems on vnykmshr</description><generator>Hugo</generator><language>en</language><lastBuildDate>Wed, 11 Mar 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://blog.vnykmshr.com/writing/categories/systems/index.xml" rel="self" type="application/rss+xml"/><item><title>Vortex architecture</title><link>https://blog.vnykmshr.com/writing/vortex-architecture/</link><pubDate>Wed, 11 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog.vnykmshr.com/writing/vortex-architecture/</guid><description>&lt;p&gt;Tesla had this thing about 3, 6, and 9. &amp;ldquo;If you only knew the magnificence of the 3, 6, and 9, then you would have a key to the universe.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;Take any doubling sequence. 1, 2, 4, 8, 16, 32, 64. Reduce each to its digital root &amp;ndash; keep adding digits until you get one number. You get 1, 2, 4, 8, 7, 5. Then it repeats. Forever. Six numbers doing all the motion, cycling endlessly.&lt;/p&gt;</description></item><item><title>The happy path</title><link>https://blog.vnykmshr.com/writing/plumbing-and-abstractions/</link><pubDate>Fri, 16 Jan 2026 00:00:00 +0000</pubDate><guid>https://blog.vnykmshr.com/writing/plumbing-and-abstractions/</guid><description>&lt;p&gt;The diagrams always show the happy path. Request goes in, response comes out, maybe a queue in between. Three boxes and two arrows. The failure modes live in the whitespace.&lt;/p&gt;
&lt;p&gt;Distributed systems work at demo time. In production, the third service fails after the first two succeeded: the debit went through, the credit went through, the write to the audit log dropped on a socket timeout, and both sides of the transfer had moved without anything recording &lt;em&gt;that&lt;/em&gt; they had moved. The same row was about to be replayed the next morning. Someone noticed the mismatch six weeks later, at the end of the month, in a spreadsheet one of the ops people was maintaining by hand.&lt;/p&gt;</description></item><item><title>Replacing OCR with Gemini</title><link>https://blog.vnykmshr.com/writing/gemini-ocr/</link><pubDate>Thu, 10 Jul 2025 00:00:00 +0000</pubDate><guid>https://blog.vnykmshr.com/writing/gemini-ocr/</guid><description>&lt;p&gt;The previous post covered an &lt;a href="https://blog.vnykmshr.com/writing/fixing-ocr-addresses/"&gt;address sanitizer&lt;/a&gt; that fixes mangled OCR output using multi-strategy matching. It works, but it&amp;rsquo;s treating a symptom. A smarter OCR step would make most of it unnecessary.&lt;/p&gt;
&lt;p&gt;Traditional OCR extracts characters, then downstream code figures out what they mean. A separate pipeline handles structure, validation, error correction. The address sanitizer is part of that pipeline. It exists because the OCR engine doesn&amp;rsquo;t understand what it&amp;rsquo;s reading.&lt;/p&gt;</description></item><item><title>Fixing OCR addresses</title><link>https://blog.vnykmshr.com/writing/fixing-ocr-addresses/</link><pubDate>Sat, 05 Jul 2025 00:00:00 +0000</pubDate><guid>https://blog.vnykmshr.com/writing/fixing-ocr-addresses/</guid><description>&lt;p&gt;OCR on government documents works well until you look at the address fields.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;State: &amp;#34;DKI JAKRTA&amp;#34;
City: &amp;#34;JAKRTA PUSAT&amp;#34;
District: &amp;#34;MENTNG&amp;#34;
Village: &amp;#34;MENTENG&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Dropped vowels, character substitutions, truncated names. Indonesian place names get mangled in predictable ways &amp;ndash; &amp;lsquo;A&amp;rsquo; becomes &amp;lsquo;R&amp;rsquo;, characters vanish mid-word. The OCR engine reads the image fine. It just can&amp;rsquo;t spell.&lt;/p&gt;
&lt;p&gt;The problem: take these broken strings and map them back to real administrative divisions. Province, city, district, village &amp;ndash; the hierarchy matters, and every level needs to resolve correctly.&lt;/p&gt;</description></item><item><title>Primary PII</title><link>https://blog.vnykmshr.com/writing/primary-pii/</link><pubDate>Tue, 05 Nov 2024 00:00:00 +0000</pubDate><guid>https://blog.vnykmshr.com/writing/primary-pii/</guid><description>&lt;p&gt;A regulation arrives. Or an auditor. Or a new market with stricter rules. PII is a thing the application was always sloppy about, and now it is a thing the application has to be careful with. This is how PII externalization begins: as someone else&amp;rsquo;s deadline, landing on the engineering team as an initiative.&lt;/p&gt;
&lt;p&gt;The work looks like encryption at first. It is not.&lt;/p&gt;
&lt;h2 id="identify"&gt;Identify&lt;/h2&gt;
&lt;p&gt;The first question is not how to encrypt. The first question is what to encrypt.&lt;/p&gt;</description></item><item><title>Circuit breaking in Go</title><link>https://blog.vnykmshr.com/writing/circuit-breaking-go/</link><pubDate>Sat, 28 Sep 2024 00:00:00 +0000</pubDate><guid>https://blog.vnykmshr.com/writing/circuit-breaking-go/</guid><description>&lt;p&gt;A service calls a dependency. The dependency is slow or down. The service waits, ties up a goroutine, maybe a connection. Multiply that by every request in flight, and the caller is now as broken as the dependency it called.&lt;/p&gt;
&lt;p&gt;Circuit breaking stops this. Instead of waiting on something that&amp;rsquo;s failing, stop calling it. Let it recover. Try again later.&lt;/p&gt;
&lt;h2 id="three-states"&gt;Three states&lt;/h2&gt;
&lt;p&gt;A circuit breaker wraps external calls and tracks their outcomes.&lt;/p&gt;</description></item><item><title>Redis caching patterns</title><link>https://blog.vnykmshr.com/writing/redis-caching-patterns/</link><pubDate>Thu, 20 Jun 2024 00:00:00 +0000</pubDate><guid>https://blog.vnykmshr.com/writing/redis-caching-patterns/</guid><description>&lt;p&gt;Put Redis in front of a database and reads get fast. The cost is a cache layer that&amp;rsquo;s now load-bearing, and a set of failure modes that come with that.&lt;/p&gt;
&lt;p&gt;Three write patterns, three hard problems. The patterns determine consistency. The problems determine whether your cache layer is a net positive or a source of outages.&lt;/p&gt;
&lt;h2 id="write-patterns"&gt;Write patterns&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Cache-aside&lt;/strong&gt; (lazy loading). The application checks cache on read. On miss, it reads from the database and populates cache. Writes go directly to the database; cache entries are either invalidated or left to expire.&lt;/p&gt;</description></item><item><title>PostgreSQL HA</title><link>https://blog.vnykmshr.com/writing/postgres-ha/</link><pubDate>Mon, 15 Mar 2021 00:00:00 +0000</pubDate><guid>https://blog.vnykmshr.com/writing/postgres-ha/</guid><description>&lt;p&gt;PostgreSQL&amp;rsquo;s streaming replication is straightforward to set up. The documentation is clear, the configuration is well-understood, and base backups with &lt;code&gt;pg_basebackup&lt;/code&gt; work reliably.&lt;/p&gt;
&lt;p&gt;The operational problems are the hard part. They show up when the primary goes down and the automated failover does the wrong thing. Or when you promote a replica that&amp;rsquo;s silently been two hours behind. Or when you discover that backups you&amp;rsquo;ve been taking for months don&amp;rsquo;t actually restore.&lt;/p&gt;</description></item><item><title>Consul in practice</title><link>https://blog.vnykmshr.com/writing/consul-in-practice/</link><pubDate>Mon, 10 Sep 2018 00:00:00 +0000</pubDate><guid>https://blog.vnykmshr.com/writing/consul-in-practice/</guid><description>&lt;p&gt;The microservice count is growing fast. The monolith is mostly gone and what replaced it is dozens of services across datacenters. We don&amp;rsquo;t have a uniform naming convention. Finding a service means knowing which team owns it, which cloud it&amp;rsquo;s on, and what they called it. That&amp;rsquo;s not scalable.&lt;/p&gt;
&lt;p&gt;Consul fixed the naming problem first.&lt;/p&gt;
&lt;h2 id="service-discovery"&gt;Service discovery&lt;/h2&gt;
&lt;p&gt;Every service registers with Consul. The DNS interface gives us a consistent way to find anything:&lt;/p&gt;</description></item><item><title>The week pgbouncer stopped being news</title><link>https://blog.vnykmshr.com/writing/pgbouncer-stopped-being-news/</link><pubDate>Thu, 12 Jul 2018 00:00:00 +0000</pubDate><guid>https://blog.vnykmshr.com/writing/pgbouncer-stopped-being-news/</guid><description>&lt;p&gt;The connection count climbs faster than our instance classes can keep up. Ops is hot. Every few weeks the same thread resurfaces: we need a pool in front of Postgres before the next scale event.&lt;/p&gt;
&lt;p&gt;We move on pgbouncer.&lt;/p&gt;
&lt;h2 id="the-choice"&gt;The choice&lt;/h2&gt;
&lt;p&gt;Two modes on the table. Session pooling hands a connection to a client and gives it back when the client disconnects. Transaction pooling hands one out per transaction. Transaction is tighter &amp;ndash; the pool stretches further, the math gets better &amp;ndash; but the client loses everything a session holds. Server-side prepared statements. Advisory locks. Temp tables. &lt;code&gt;SET&lt;/code&gt; commands that expect to persist.&lt;/p&gt;</description></item><item><title>The GraphQL buffer</title><link>https://blog.vnykmshr.com/writing/the-graphql-buffer/</link><pubDate>Fri, 20 Apr 2018 00:00:00 +0000</pubDate><guid>https://blog.vnykmshr.com/writing/the-graphql-buffer/</guid><description>&lt;p&gt;The GraphQL gateway started as a practical problem. We had mobile apps, web clients, and a growing number of backend services. Every client talked to every backend directly. When a new backend came up or an old one changed its API, every client needed updating. The gateway was supposed to fix that &amp;ndash; one schema, one endpoint, clients talk to GraphQL, GraphQL talks to backends.&lt;/p&gt;
&lt;p&gt;We built it in Go, starting from a fork of &lt;code&gt;graphql-go&lt;/code&gt;. The fork grew over time &amp;ndash; custom resolvers, caching layers, request batching, things we needed that the upstream didn&amp;rsquo;t have. We&amp;rsquo;d sync the fork every few months, but our changes kept growing. Five of us on the team, and most of the early days went into getting other teams to migrate their APIs onto the gateway. We built the base, got teams to add and own their own modules, then moved into a gatekeeping role &amp;ndash; reviewing what went in, making sure the schema stayed coherent.&lt;/p&gt;</description></item><item><title>The first service</title><link>https://blog.vnykmshr.com/writing/carving-the-first-service/</link><pubDate>Wed, 15 Mar 2017 00:00:00 +0000</pubDate><guid>https://blog.vnykmshr.com/writing/carving-the-first-service/</guid><description>&lt;p&gt;The monolith is Perl. No framework I can identify &amp;ndash; just a large codebase that&amp;rsquo;s been growing for years. One Perl line does an alarming amount. I&amp;rsquo;m not a Perl developer and had to read through the code several times before I was confident I understood what it was doing.&lt;/p&gt;
&lt;p&gt;Production goes down for days sometimes. When it does, the team spends hours tracing through the code to figure out what broke. That&amp;rsquo;s the context. The system works, mostly. When it doesn&amp;rsquo;t, nobody quite knows why.&lt;/p&gt;</description></item><item><title>Two-sided graph</title><link>https://blog.vnykmshr.com/writing/two-sided-graph/</link><pubDate>Sat, 15 Nov 2014 00:00:00 +0000</pubDate><guid>https://blog.vnykmshr.com/writing/two-sided-graph/</guid><description>&lt;p&gt;The platform started with one side &amp;ndash; users making plans, tagging friends, joining each other&amp;rsquo;s plans. The graph model handles that well. Users connect through &lt;code&gt;KNOWS&lt;/code&gt; edges, plans connect through &lt;code&gt;CREATED&lt;/code&gt;, &lt;code&gt;JOINED&lt;/code&gt;, &lt;code&gt;TAGGED&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Now we&amp;rsquo;re adding the other side. Businesses. A cafe with a slow Tuesday wants to drop a deal into the plans forming around it. A venue can offer a discount to a group that&amp;rsquo;s already half-organized. The product idea from the beginning was that both sides would share the same surface &amp;ndash; people with half-formed plans and businesses with empty hours, meeting in the same feed.&lt;/p&gt;</description></item><item><title>Running two systems at once</title><link>https://blog.vnykmshr.com/writing/running-two-systems-at-once/</link><pubDate>Tue, 20 Nov 2012 00:00:00 +0000</pubDate><guid>https://blog.vnykmshr.com/writing/running-two-systems-at-once/</guid><description>&lt;p&gt;We&amp;rsquo;re replacing the frontend of a running store. The old system handles everything &amp;ndash; catalog, cart, checkout, orders, fulfillment. The new one is Node.js. We can&amp;rsquo;t switch over in a weekend. The store has traffic every day, orders every hour. Nobody&amp;rsquo;s going to approve a &amp;ldquo;shut it down Friday, bring up the new one Monday&amp;rdquo; plan.&lt;/p&gt;
&lt;p&gt;So both systems run at the same time. On the same domain, behind the same nginx. Writing this down before I forget the order we did it in.&lt;/p&gt;</description></item><item><title>Rolling your own search</title><link>https://blog.vnykmshr.com/writing/rolling-your-own-search/</link><pubDate>Wed, 14 Mar 2012 00:00:00 +0000</pubDate><guid>https://blog.vnykmshr.com/writing/rolling-your-own-search/</guid><description>&lt;p&gt;The shop needed catalog search. Users type something, products come back. Sounds trivial until you start building it.&lt;/p&gt;
&lt;p&gt;Our stack is Node.js, MySQL for the primary data store, MongoDB for everything else we need to go fast. The catalog lives in MySQL &amp;ndash; products, categories, attributes, prices. Normalized, relational, correct. But relational isn&amp;rsquo;t searchable. Try finding &amp;ldquo;blue cotton kurta&amp;rdquo; across five joined tables with MySQL FULLTEXT on MyISAM. It sort of works. The relevance is terrible.&lt;/p&gt;</description></item></channel></rss>