<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Umair Abid</title>
    <description>The latest articles on Forem by Umair Abid (@umairabid).</description>
    <link>https://forem.com/umairabid</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1169120%2Fb18f07a4-df3c-4c5c-84c2-92018fa6ba34.png</url>
      <title>Forem: Umair Abid</title>
      <link>https://forem.com/umairabid</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/umairabid"/>
    <language>en</language>
    <item>
      <title>Introducing LazySQL: A Database Client for the Shell</title>
      <dc:creator>Umair Abid</dc:creator>
      <pubDate>Sat, 16 May 2026 00:00:00 +0000</pubDate>
      <link>https://forem.com/umairabid/introducing-lazysql-a-database-client-for-the-shell-1d4o</link>
      <guid>https://forem.com/umairabid/introducing-lazysql-a-database-client-for-the-shell-1d4o</guid>
      <description>&lt;p&gt;I have always had an urge to keep my system memory as low as possible. It started out of necessity, my dev machine wasn’t powerful enough to spare any but the habit stuck even after I got faster hardware. IDEs and browsers kept growing, and the rest of the stack didn’t help. Docker, VS Code, a database client, a git client, Postman, plus whatever I was profiling that day. If a background job was under the microscope, I was sitting at 99% memory usage. And it wasn’t just memory — switching between windows, juggling terminals inside the IDE, tailing logs in emulators, that was its own ongoing cost.&lt;/p&gt;

&lt;p&gt;I avoided Vim for years. The shame of not being able to close one was too much. But a couple of years ago I started self-hosting bits of my own observability stack instead of using the off-the-shelf ones, and that forced the issue. Small things at first — editing nginx configs, tweaking Dockerfiles, navigating log files. My config grew. Eventually I could do most of my work inside Vim. After I picked up tmux for building little dashboards, the toolkit felt complete. On my time off, I was running my whole workflow inside the terminal. Browsers started showing up late in the loop, which is what you’d expect from a TDD habit.&lt;/p&gt;

&lt;p&gt;The one piece that wouldn’t fit was the database client.&lt;/p&gt;

&lt;h1&gt;
  
  
  Why a new database client
&lt;/h1&gt;

&lt;p&gt;Database clients were the last sharp edge in my terminal-first setup, and they had more than one pain point:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Most of the good ones are licensed.&lt;/strong&gt; Each company I worked at had picked a different one depending on the stack — and if you were on a NoSQL like Dynamo, you were on whatever vendor tooling came with it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;They are big.&lt;/strong&gt; Most are general-purpose, with far more surface area than an application developer needs day to day.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;They don’t move like Vim.&lt;/strong&gt; My hands have a strong opinion about motion keys, and editing queries in a non-Vim text box is friction that compounds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Credentials rotate.&lt;/strong&gt; In most modern environments, even dev credentials live in a vault and get rotated. That means a constant loop of copy-paste-edit just to stay connected.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What I actually wanted was a hackable, minimal client with Vim bindings — the same shape that &lt;code&gt;lazygit&lt;/code&gt; has for git. There was one open-source TUI client in the space, but the UI wasn’t quite to my taste and it was missing the features I cared about. I had recently finished an experimental Arch Linux installer, so building another thing from scratch felt reasonable. LazySQL was born out of that.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fumairabid.com%2Fassets%2Fimg%2Fp4-demo.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fumairabid.com%2Fassets%2Fimg%2Fp4-demo.gif" alt="LazySQL demo" width="800" height="467"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  What LazySQL is
&lt;/h1&gt;

&lt;p&gt;LazySQL is a TUI written in Go on top of &lt;a href="https://github.com/charmbracelet/bubbletea" rel="noopener noreferrer"&gt;&lt;code&gt;bubbletea&lt;/code&gt;&lt;/a&gt;and &lt;a href="https://github.com/kujtimiihoxha/vimtea" rel="noopener noreferrer"&gt;&lt;code&gt;vimtea&lt;/code&gt;&lt;/a&gt;. The current release is alpha and focused on PostgreSQL, but the internals are deliberately small and the driver layer is pluggable.&lt;/p&gt;

&lt;p&gt;Three panes, one footer, no gui.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Left:&lt;/strong&gt; the explorer — databases, schemas, tables, and the table’s data / schema / indexes underneath them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Top-right:&lt;/strong&gt; the query editor, a real Vim buffer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bottom-right:&lt;/strong&gt; the results viewer, scrollable in both axes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  The pieces I cared most about getting right
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Three connection modes, including a command mode.&lt;/strong&gt; You can configure a connection with static credentials, with a connection URL, or — the one I built this for — by pointing LazySQL at a shell command that prints credentials on stdout. The command runs every time you connect, which means rotating-secret workflows (Vault, AWS RDS IAM, GCP IAM, short-lived dev tokens) stop being a copy-paste loop and become a one-time configuration.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Credentials go in the OS keyring when available.&lt;/strong&gt; Passwords and any URLs that contain credentials are stored in the platform keyring — macOS Keychain, Windows Credential Locker, or Linux Secret Service. If no keyring is available (headless Linux, for instance), LazySQL falls back to plain JSON. That fallback is a deliberate choice — it keeps the tool usable in environments where a keyring isn’t running — but it’s a fallback, not a default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bindings borrowed from Vim and lazygit.&lt;/strong&gt; &lt;code&gt;h j k l&lt;/code&gt; to move, &lt;code&gt;?&lt;/code&gt; for help, &lt;code&gt;Shift+Tab&lt;/code&gt; to cycle panes. The query editor is a full Vim buffer via &lt;code&gt;vimtea&lt;/code&gt;, so modes, motions, and visual selection all work the way your fingers expect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Explorer navigation borrowed from &lt;a href="https://github.com/lambdalisue/fern.vim" rel="noopener noreferrer"&gt;&lt;code&gt;vim-fern&lt;/code&gt;&lt;/a&gt;.&lt;/strong&gt;&lt;code&gt;l&lt;/code&gt; to expand a node, &lt;code&gt;h&lt;/code&gt; to collapse. When you reach a leaf — a table’s data, schema, or indexes — the result loads into the viewer pane. No modal popups, no separate window.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run-selection in the editor.&lt;/strong&gt; Select a query in visual mode, hit&lt;code&gt;Ctrl+r&lt;/code&gt;, and that fragment runs. &lt;code&gt;vimtea&lt;/code&gt; didn’t support this out of the box, so it lives in a fork right now; a PR is open upstream.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No built-in LLM chat.&lt;/strong&gt; This was intentional. I don’t think every app having its own chat box is sustainable. Instead, each LazySQL session writes a log file at &lt;code&gt;~/.config/lazysql/sessions/session-&amp;lt;PID&amp;gt;.log&lt;/code&gt; — the queries you ran, the schemas you touched, the kind of context an agent actually needs. The PID is in the footer. Pipe the file into Claude, Gemini, or whatever you use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; ~/.config/lazysql/sessions/session-&amp;lt;pid&amp;gt;.log | claude &lt;span class="s2"&gt;"summarize what I was doing"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Logs from a normally exited session are cleaned up on quit. Logs from dead processes are cleaned up on the next startup. If you want to keep one, copy it out while the session is alive.&lt;/p&gt;

&lt;h1&gt;
  
  
  What’s next
&lt;/h1&gt;

&lt;p&gt;A few directions I expect to push this in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;More drivers.&lt;/strong&gt; MySQL is next, then Dynamo. The adapter layer is the obvious place to extend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;First-class hooks for LLM agents.&lt;/strong&gt; Beyond the session log: things like running queries from an agent against a specific live instance, so the agent has somewhere to &lt;em&gt;act&lt;/em&gt;, not just somewhere to read.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The usual.&lt;/strong&gt; Bug fixes, performance, and the requests that come in from people actually using it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The repo is at &lt;a href="https://github.com/umairabid/lazysql" rel="noopener noreferrer"&gt;github.com/umairabid/lazysql&lt;/a&gt;. It’s early — issues and PRs are welcome.&lt;/p&gt;

</description>
      <category>tui</category>
      <category>go</category>
      <category>bubbletea</category>
      <category>database</category>
    </item>
    <item>
      <title>Adding a Change-Log System Without Breaking the One You Have</title>
      <dc:creator>Umair Abid</dc:creator>
      <pubDate>Thu, 15 Jan 2026 00:00:00 +0000</pubDate>
      <link>https://forem.com/umairabid/adding-a-change-log-system-without-breaking-the-one-you-have-16fe</link>
      <guid>https://forem.com/umairabid/adding-a-change-log-system-without-breaking-the-one-you-have-16fe</guid>
      <description>&lt;p&gt;Customers wanted to know who did what, and when, and they wanted to know it without having to ask us. Specifically: was that change made by a human, or by automation? When two stakeholders disagreed, the default move had become to ask engineering to dig through logs. Not sustainable.&lt;/p&gt;

&lt;p&gt;The ask sounded simple — “log changes to objects” — but the constraint that made it interesting was that we were adding this to a system that was already running. The audit path had to do its job without breaking the path being audited.&lt;/p&gt;

&lt;h1&gt;
  
  
  The shape of the problem
&lt;/h1&gt;

&lt;p&gt;What we actually needed was:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A record per change, with &lt;strong&gt;what&lt;/strong&gt; changed, on &lt;strong&gt;which object&lt;/strong&gt; , by &lt;strong&gt;which actor&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The ability to ask “show me everything that happened to this object” — quickly, and across a lot of history.&lt;/li&gt;
&lt;li&gt;Zero impact on the latency of the operations being audited.&lt;/li&gt;
&lt;li&gt;A failure in the audit path that does &lt;em&gt;not&lt;/em&gt; leak into the operation it’s auditing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The closer we got to that list, the more it looked like a side system that happened to share a database key with the main app — not a feature inside it.&lt;/p&gt;

&lt;h1&gt;
  
  
  Capturing changes the right way
&lt;/h1&gt;

&lt;p&gt;The codebase had been moving toward a command pattern: each business operation fulfilled by a small object that owned its context. That turned out to be the lever we needed.&lt;/p&gt;

&lt;p&gt;We wrote a concern that any command could mix in, and the concern took care of the boring parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Snapshot the model before the mutation, snapshot it again after. Diff. That’s the “what changed.”&lt;/li&gt;
&lt;li&gt;Pull the current actor and the current user out of context. These are not always the same person.&lt;/li&gt;
&lt;li&gt;Build a payload — either from the active record directly, or via an adapter for cases where the change spanned multiple models or lived deeper than a single object.&lt;/li&gt;
&lt;li&gt;Schedule a background job to persist the change-log entry.&lt;/li&gt;
&lt;li&gt;Enrich the payload after the fact: turn &lt;code&gt;user_id: 7&lt;/code&gt; into&lt;code&gt;user: "My Name"&lt;/code&gt; so anyone reading the log later doesn’t need another query to make sense of it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The concern existed so commands stayed readable. The thing a command_looks_ like, in code, is the business operation — not the bookkeeping.&lt;/p&gt;

&lt;h2&gt;
  
  
  Actor vs. user (the distinction that matters)
&lt;/h2&gt;

&lt;p&gt;The single most important thing the change-log captured wasn’t_what_ changed. It was &lt;strong&gt;who did it&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;There is a difference between the user whose data was affected and the actor who performed the change. For self-service flows they’re the same. For impersonation, API tokens, and automation, they are not — and the whole reason customers wanted the audit trail was to tell those cases apart.&lt;/p&gt;

&lt;p&gt;If we’d modeled this as a single &lt;code&gt;user_id&lt;/code&gt;, we’d have shipped a product that couldn’t answer the question it was built for. The moment we got that distinction right in the data model, the rest got noticeably easier.&lt;/p&gt;

&lt;h1&gt;
  
  
  The constraints that shaped the architecture
&lt;/h1&gt;

&lt;p&gt;Three things forced most of the design:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Operations were already near their SLA.&lt;/strong&gt; A bunch of the operations we wanted to audit had p95 latencies that didn’t leave us room to do extra synchronous work. That ruled out writing the change log inline. So: every persist goes through a background job. The command captures, the worker stores.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An audit failure must not become an operation failure.&lt;/strong&gt; “Recording that you did the thing” cannot break “doing the thing.” That meant strict isolation: the change-log worker has its own queue, its own dashboards, its own alerts. If it falls over, nothing in the user-facing path notices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Operations performed in the background lose their user context.&lt;/strong&gt; Workers don’t have a session attached. We had to make peace with the fact that for some backend-only operations, the actor is going to be a system default rather than a real person. Pretending otherwise would have meant lying in the audit log.&lt;/p&gt;

&lt;h1&gt;
  
  
  Why DynamoDB
&lt;/h1&gt;

&lt;p&gt;This was the most contested decision and the one I’m most sure about in hindsight.&lt;/p&gt;

&lt;p&gt;The shape of the queries was:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“Everything for object X, newest first.”&lt;/li&gt;
&lt;li&gt;Append-heavy, read-rarely.&lt;/li&gt;
&lt;li&gt;Going to grow forever.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The shape we did &lt;em&gt;not&lt;/em&gt; need was joins. A change-log entry doesn’t join to anything; the payload is denormalized at write time, on purpose, so that years later we don’t accidentally show stale context because some related record got renamed.&lt;/p&gt;

&lt;p&gt;That’s a fairly precise fit for a key-value store with sortable range keys, and a fairly bad fit for the main relational database that was already under load from the rest of the product. Putting this in Dynamo meant the audit table could grow without competing for resources with the primary database. It cost us some infrastructure complexity. It bought us the ability to forget about the change-log table when we were tuning anything else.&lt;/p&gt;

&lt;h1&gt;
  
  
  Legacy paths got a wrapper
&lt;/h1&gt;

&lt;p&gt;Most of the API had moved to the command pattern, but a chunk of the legacy API hadn’t. We found that out later than we’d have liked.&lt;/p&gt;

&lt;p&gt;Rewriting the legacy API to use commands was a separate, larger project that wasn’t going to ship in time for this one. So we built a wrapper: the legacy API hands an action and a context to it, the wrapper builds an object that walks and talks like a command, mixes in the same concern, and calls the same log function.&lt;/p&gt;

&lt;p&gt;It’s a shim. It will get deleted when the underlying API gets modernized. Until then it means there is exactly one path that writes change-log entries, which is the property worth protecting.&lt;/p&gt;

&lt;h1&gt;
  
  
  Rolling it out without making any noise
&lt;/h1&gt;

&lt;p&gt;We turned the change log on one action at a time, behind a flag. Each action’s worker had its own dashboard. We’d flip the flag, watch the queue depth and error rate for a few hours, then move on to the next action. If something looked off, the flag came back off and the operation went back to behaving exactly as it always had.&lt;/p&gt;

&lt;p&gt;By the end of the rollout the audit trail was complete, and no customer had noticed anything had changed — which, for the audit log, is the highest compliment.&lt;/p&gt;

&lt;h1&gt;
  
  
  What I’d take to the next one
&lt;/h1&gt;

&lt;p&gt;A few things that I think generalize:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Move side-system writes off the user request path.&lt;/strong&gt; A background job, with its own queue and its own observability, buys you both performance headroom and isolation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick the data model for the actual query shape.&lt;/strong&gt; A growing, append-heavy, no-joins workload does not belong on your main relational DB just because that’s where everything else lives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate actor from user, in the data, on day one.&lt;/strong&gt; The difference between “who is this for” and “who did this” is the difference between an audit log that answers questions and one that creates them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrap, don’t rewrite, when you have to ship.&lt;/strong&gt; A focused shim around legacy code is allowed to exist if it preserves a single canonical write path. Just be honest about it being a shim.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Reshaping an Invoice-Sync Pipeline Without a Rewrite</title>
      <dc:creator>Umair Abid</dc:creator>
      <pubDate>Thu, 08 May 2025 00:00:00 +0000</pubDate>
      <link>https://forem.com/umairabid/reshaping-an-invoice-sync-pipeline-without-a-rewrite-pbf</link>
      <guid>https://forem.com/umairabid/reshaping-an-invoice-sync-pipeline-without-a-rewrite-pbf</guid>
      <description>&lt;p&gt;We had a working sync framework. The shape was simple: one local entity, one remote entity, push changes across when they diverged. It served us well for years. Then the product team showed up with a new invoicing workflow that did not fit that shape at all.&lt;/p&gt;

&lt;p&gt;The new flow was:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Customers create quotes (and a separate thing we ended up calling “MR quotes”).&lt;/li&gt;
&lt;li&gt;Customers can accept prepayments against those quotes.&lt;/li&gt;
&lt;li&gt;Quotes get synced downstream as the canonical record.&lt;/li&gt;
&lt;li&gt;As services are rendered, line items get posted against the quote.&lt;/li&gt;
&lt;li&gt;Those charges deduct from the prepaid balance.&lt;/li&gt;
&lt;li&gt;Once services are complete, a final invoice is generated and sent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A single local quote could end up touching multiple remote entities, in a specific order, sometimes weeks apart. That’s a 1:N sync problem, and our framework only knew how to do 1:1.&lt;/p&gt;

&lt;p&gt;The lazy option was to write parallel sync logic &lt;em&gt;next to&lt;/em&gt; the framework. Build a second pipeline for invoices and let the original keep doing its thing. That would have worked for about six months and then we’d have two pipelines slowly diverging in subtle ways. So we decided to stretch the framework instead.&lt;/p&gt;

&lt;h1&gt;
  
  
  What got built first turned out to be the easy part
&lt;/h1&gt;

&lt;p&gt;The visible work — adding the new entities, wiring up the prepayment flow, making sure each line item posted to the right remote object — was the part we estimated up front. It went roughly as planned.&lt;/p&gt;

&lt;p&gt;The interesting work was everything we found &lt;em&gt;after&lt;/em&gt; the first version was on staging.&lt;/p&gt;

&lt;h2&gt;
  
  
  Async jobs failing randomly, creating duplicates
&lt;/h2&gt;

&lt;p&gt;The first thing the QA cycle turned up was that some sync jobs were silently failing and then re-running, and the re-run was sometimes creating a second copy of a charge downstream. Not always. Just often enough to be terrifying.&lt;/p&gt;

&lt;p&gt;We considered a few things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Exponential backoff with sleeps.&lt;/strong&gt; Tempting, but our workers are not infinite. A backed-off sleep is a worker you can’t use for anything else, and a queue that gets choked by retries during an outage is worse than the outage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Splitting each sync job into smaller jobs.&lt;/strong&gt; Cleaner in theory, more correct under failure. The amount of refactoring to get there was not reasonable given what else was on the roadmap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Making the jobs idempotent.&lt;/strong&gt; Pick a stable external key, check before you write, and treat a re-run as a no-op if the work has already landed. Cheap to implement, and “this can run twice without consequences” is a property worth having for its own sake.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We went with idempotent + retries. The duplicate-charge bug disappeared. More importantly, the next two async bugs we found also disappeared on their own, because we’d already made the operations safe to repeat.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decimal places that didn’t agree with the accounting system
&lt;/h2&gt;

&lt;p&gt;Some invoices were off by a cent or two. Sometimes more. The cause turned out to be the price calculator: we were storing prices as floats with effectively unlimited precision and rounding at the very end. The downstream accounting system rounded at every line.&lt;/p&gt;

&lt;p&gt;We branched the calculator behind a feature flag — old behavior for existing data, fixed behavior for new — and slowly moved customers across. Now: every monetary calculation happens in the minimum currency unit (cents), and rounding happens at the same boundary it happens at downstream. There is no graceful way to retrofit this into a system that was happily doing float math in production, so the feature flag earned its keep.&lt;/p&gt;

&lt;h2&gt;
  
  
  Silent failures when the posting window closed
&lt;/h2&gt;

&lt;p&gt;Some charges weren’t posting at all, and we didn’t notice for days, because the failure was silent: the downstream system had a “posting window” (essentially a billing period), and once a window closed, anything submitted against it was rejected without a useful error.&lt;/p&gt;

&lt;p&gt;The fix was partly observability — alert on the rejected-write shape so we’d see it within minutes — and partly workflow: detect the closed-window state before sending and route those charges into a different reconciliation path.&lt;/p&gt;

&lt;h1&gt;
  
  
  Two design decisions that paid off
&lt;/h1&gt;

&lt;p&gt;A couple of choices, made early enough to matter, kept the system from collapsing under the weight of the new flow:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prepayment as its own type.&lt;/strong&gt; The shortcut was to add a&lt;code&gt;prepayment_percentage&lt;/code&gt; column to the existing invoice model and move on. We took the slower route: prepayment got its own type. It cost some brevity at the model level, but every downstream consumer — the sync, the locking, the state machine, reporting — could now tell at a glance what it was looking at. There was no “is this &lt;em&gt;really&lt;/em&gt; a prepayment, or just an invoice with a percentage set?” branch in any code path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A real state machine instead of boolean flags.&lt;/strong&gt; Locking an invoice once it had been sent downstream started life as a single boolean column. By the time we were done, the lifecycle had at least five states with constraints on which transitions were allowed. Replacing the boolean with an explicit state machine made the sync logic — “sync this thing if it’s in state X, ignore it if it’s in state Y, queue a follow-up if it’s in state Z” — fall out of the model rather than being scattered across the codebase.&lt;/p&gt;

&lt;h1&gt;
  
  
  Stretching the framework instead of forking it
&lt;/h1&gt;

&lt;p&gt;The biggest architectural decision was the framework one. We taught the existing sync framework to support 1:N relationships rather than writing a second pipeline.&lt;/p&gt;

&lt;p&gt;The reason wasn’t elegance. It was risk. A second pipeline meant a second place to monitor, a second place where retries could go wrong, and a second team mental model to keep loaded. Stretching the framework was more work up front and more careful work — but every existing capability (bulk operations, retry behavior, observability hooks) came along with it for free.&lt;/p&gt;

&lt;p&gt;If I had to summarize the whole project in one sentence: most of the real work was the work we discovered after the happy path was already running, and the early architectural choices were what determined whether discovering it cost us a week or a quarter.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>softwareengineering</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Traveling time with Postgres Range Columns</title>
      <dc:creator>Umair Abid</dc:creator>
      <pubDate>Mon, 23 Oct 2023 00:00:00 +0000</pubDate>
      <link>https://forem.com/umairabid/traveling-time-with-postgres-range-columns-35i</link>
      <guid>https://forem.com/umairabid/traveling-time-with-postgres-range-columns-35i</guid>
      <description>&lt;p&gt;In &lt;a href="///2023/09/06/temporal-system-challenges.html"&gt;Challenges of Time-Based Systems Without Proper Database Structures&lt;/a&gt;, we looked into everything that went wrong when we tried to build a temporal system without a compatible foundation. In this article, we will describe how we added that foundation to support temporal use cases. We will start by discussing how we built the foundation using Postgres ranges that could be a potential denominator for any time-based system. The solution might not be general enough but it can provide some good insights for building a foundation for the temporal system.&lt;/p&gt;

&lt;h1&gt;
  
  
  Migrating First Table
&lt;/h1&gt;

&lt;p&gt;We started by migrating the &lt;code&gt;state_taxes&lt;/code&gt; as it contained fewer rows and had fewer dependencies than other tables. The reason for starting with a relatively simple table was to vet the solution with minimum dependencies and then expand to other tables. The first version of the table structure we came up with was as follows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state_taxes&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;nextval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'state_taxes_id_seq'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;regclass&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;state_id&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;tax_type&lt;/span&gt; &lt;span class="nb"&gt;character&lt;/span&gt; &lt;span class="nb"&gt;varying&lt;/span&gt; &lt;span class="k"&gt;COLLATE&lt;/span&gt; &lt;span class="n"&gt;pg_catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"default"&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;rate&lt;/span&gt; &lt;span class="nb"&gt;numeric&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;effective_range&lt;/span&gt; &lt;span class="n"&gt;daterange&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;system_range&lt;/span&gt; &lt;span class="n"&gt;tsrange&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;state_taxes_pkey&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;prevent_overlapping_state_taxes&lt;/span&gt; &lt;span class="n"&gt;EXCLUDE&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;gist&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="n"&gt;system_range&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;state_id&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;effective_range&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;tax_type&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Understanding State Taxes Structure
&lt;/h1&gt;

&lt;p&gt;The key and important difference from the previous version is two columns &lt;code&gt;effective_range&lt;/code&gt; and &lt;code&gt;system_range&lt;/code&gt; with the addition of the constraint &lt;code&gt;prevent_overlapping_state_taxes&lt;/code&gt;. Let’s go through each of them and see what value they add&lt;/p&gt;

&lt;h2&gt;
  
  
  Effective Range Column
&lt;/h2&gt;

&lt;p&gt;This column unlocks the ability to create timelines by having a rate for a specific start and end date, eliminating the need for year the column. The clients will add rates only by providing a start date and the backend system will automatically detect the end date for the rate. The benefit of using range columns is that querying becomes easier using powerful &lt;a href="https://www.postgresql.org/docs/9.3/functions-range.html?ref=umairabid.com" rel="noopener noreferrer"&gt;Postgres range functions&lt;/a&gt;. For example, if a client asks for a rate on a specific effective date we can easily find it by searching a row whose effective range overlaps with the provided effective date.&lt;/p&gt;

&lt;h2&gt;
  
  
  System Range Column
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;system_range&lt;/code&gt; helps us solve the shoe store problem discussed in the last article. This column stores the validity of data in terms of system time, also in the form of a range with specific start and end dates. When a rate is added, the system will set the current time at the time of change as the start of the validity range. Later if the rate is invalidated, the system will set the end time as the end of the system range when the change was made. This eliminates any need for maintaining &lt;code&gt;deleted_at&lt;/code&gt; columns. The system range actually removes the concept of soft deletes and replaces it with versioning the data with system validity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exclude Constraint
&lt;/h2&gt;

&lt;p&gt;You can think of this constraint as a unique constraint but since ranges are involved and we want to check for overlapping ranges, the exclude constraint was used. Exclude constraint basically doesn’t allow two rows to exist that return true for the provided gist condition. This helps us ensure we only get one valid row for one effective date.&lt;/p&gt;

&lt;h1&gt;
  
  
  Adding Timeline Logic to State Taxes
&lt;/h1&gt;

&lt;p&gt;With a solid underlying table structure to support temporal operations next step was to add logic to &lt;code&gt;StateTaxes&lt;/code&gt; model which will ensure the timeline logic of changes as they are added. We defined the following expectations for handling changes&lt;/p&gt;

&lt;h2&gt;
  
  
  First Change
&lt;/h2&gt;

&lt;p&gt;If a rate is added for state tax for the first time for the effective date let’s say &lt;code&gt;2023-01-01&lt;/code&gt; we expected the following record in the table&lt;/p&gt;

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

&lt;p&gt;This row tells us that the rate 0.15 is effective from 2023-01-01 till the end of time and it is valid from 2023-10-16 (the time it was added) to the end of time, for state_id=1 and tax_type=income_tax (identified unique tax rate). This statement can be understood by a few queries, let’s ask the system for a rate effective on 2023-05-01&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt; 
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;state_taxes&lt;/span&gt; 
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;state_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; 
  &lt;span class="n"&gt;tax_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'income_tax'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt;
  &lt;span class="n"&gt;effective_range&lt;/span&gt; &lt;span class="o"&gt;@&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'2023-05-01'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;

&lt;span class="o"&gt;#=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This seems correct since the rate is effective from 2023-01-01 to end of time, let’s ask for the rate before this date\&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt; 
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;state_taxes&lt;/span&gt; 
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;state_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; 
  &lt;span class="n"&gt;tax_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'income_tax'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt;
  &lt;span class="n"&gt;effective_range&lt;/span&gt; &lt;span class="o"&gt;@&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'2022-12-31'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;

&lt;span class="o"&gt;#=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As expected since the date is before the date the first rate is effective, the query returned nil. Now let’s query for any rates valid in the system time before the date 2023-10-16&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt; 
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;state_taxes&lt;/span&gt; 
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;state_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; 
  &lt;span class="n"&gt;tax_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'income_tax'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt;
  &lt;span class="n"&gt;system_range&lt;/span&gt; &lt;span class="o"&gt;@&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'2022-10-16'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt;

&lt;span class="o"&gt;#=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;nil&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This returns nil because as far as the system is concerned no rate existed in the system time for 2023-10-16, this is how it helps in the example of a shoe store by finding rates when transactions occurred in the system.&lt;/p&gt;

&lt;h2&gt;
  
  
  After First Change
&lt;/h2&gt;

&lt;p&gt;If the first change is already added the rest of the changes will fall in one or a combination of the following scenarios&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The new change has the same effective date as the effective date (Correction)&lt;/li&gt;
&lt;li&gt;The new change effective date is before the existing change effective date (Past Change)&lt;/li&gt;
&lt;li&gt;The new change effective date is after the existing change effecting date (Future Change)&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Adding a correction
&lt;/h2&gt;

&lt;p&gt;When a new change has the same effective date as an existing change, we need to invalidate the existing change and replace it with a new one. It is called a correction because the new change replaced the old one. If we correct our first change rate from 0.15 to 0.19 the result will look like something below&lt;/p&gt;

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

&lt;p&gt;It shows that we invalidated our first change by adding an end to system_range of the first change and then added the correction with the new rate. Now if only query valid rates effective on or after 2023-01-01 we get 0.19&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt; 
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;state_taxes&lt;/span&gt; 
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;state_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; 
  &lt;span class="n"&gt;tax_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'income_tax'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt;
  &lt;span class="k"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;effective_range&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2023-01-01'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt;
  &lt;span class="k"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;system_range&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="o"&gt;#&lt;/span&gt; &lt;span class="k"&gt;only&lt;/span&gt; &lt;span class="k"&gt;valid&lt;/span&gt; &lt;span class="n"&gt;rates&lt;/span&gt; &lt;span class="n"&gt;have&lt;/span&gt; &lt;span class="n"&gt;system_range&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;

&lt;span class="o"&gt;#=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;19&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Adding a Past Change
&lt;/h2&gt;

&lt;p&gt;When a new change is added whose effective date is before the already existing change, then the new change should automatically assume an end date as well. This makes sure that end result is a consistent timeline where effective ranges don’t overlap. For example, continuing from before, if we add a change for the effective date 2022-12-01 with rate 0.14 then execute the query&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; 
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;state_taxes&lt;/span&gt; 
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;state_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; 
  &lt;span class="n"&gt;tax_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'income_tax'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt;
  &lt;span class="k"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;system_range&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;effective_range&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It will return the following result&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Adding a Future Change
&lt;/h2&gt;

&lt;p&gt;When a change is added whose effective date is after the existing change, the existing change needs to have a new end date. So in order to apply the change, we correct the existing change by replacing it with a new end date. Now in our example if we add a rate 0.25 with effective date 2023-02-01 the query in the previous example will return the following result&lt;/p&gt;

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

&lt;p&gt;For reference fetching changes including the invalidated ones results in the below&lt;/p&gt;

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

&lt;p&gt;You can find the implementation for the rails model &lt;a href="https://gist.github.com/umairabid/54ca1f6ab7a32439554551418847ced5?ref=umairabid.com" rel="noopener noreferrer"&gt;here&lt;/a&gt; and &lt;a href="https://gist.github.com/umairabid/7fe9619d73e0a17558145b5d4fe6e9fe?ref=umairabid.com" rel="noopener noreferrer"&gt;migration&lt;/a&gt; here to run examples by yourself.&lt;/p&gt;

&lt;h1&gt;
  
  
  Scaling beyond State Tax Table
&lt;/h1&gt;

&lt;p&gt;After completing the implementation for the state tax table, the next task was to assess how this implementation would work when joining tables and how the same implementation could be applied to other tables. We immediately saw that we needed to modify our approach or rethink our table relations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem with Relations
&lt;/h2&gt;

&lt;p&gt;Initially before adding effectivity to state_tax table, the id was an explicit primary key to identifying a unique tax rate, whereas the composite key (state_id, tax_type) served as the implicit primary key. However, with the new structure, the id was no longer the key to identify a tax rate hence won’t work as a foreign key meant to identify a unique tax, and reason why we had to resort to using the composite key to identify taxes.&lt;/p&gt;

&lt;p&gt;The nature of the issue can be traced to the fact that before the change each row state_tax was one “tax rate” but after, a row was one “tax rate change”. In other words, after changing the structure the table should also have been renamed to state_tax_changes. To fix the relations we thought about just having a running id in the table to be used as the foreign key in the related tables. Still, the insight that we have fundamentally changed the table prevented us from continuing with the running id hack.&lt;/p&gt;

&lt;h2&gt;
  
  
  Splitting the Tables
&lt;/h2&gt;

&lt;p&gt;To resolve the relations as they were defined currently we decided to not replace tables but rather split tables into the main model and its effective attributes. So effective attributes of state_taxes were moved to another table state_tax_changes. The resulting table structures looked something like the ones below&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state_taxes&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;nextval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'state_taxes_id_seq'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;regclass&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;state_id&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tax_type&lt;/span&gt; &lt;span class="nb"&gt;character&lt;/span&gt; &lt;span class="nb"&gt;varying&lt;/span&gt; &lt;span class="k"&gt;COLLATE&lt;/span&gt; &lt;span class="n"&gt;pg_catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"default"&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state_tax_changes&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;nextval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'state_taxes_id_seq'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;regclass&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;state_tax_id&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;NUL&lt;/span&gt;
    &lt;span class="n"&gt;rate&lt;/span&gt; &lt;span class="nb"&gt;numeric&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;effective_range&lt;/span&gt; &lt;span class="n"&gt;daterange&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;system_range&lt;/span&gt; &lt;span class="n"&gt;tsrange&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;state_tax_changes_pkey&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;prevent_overlapping_state_taxes&lt;/span&gt; &lt;span class="n"&gt;EXCLUDE&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;gist&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;state_tax_id&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;effective_range&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;tax_type&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Although from the implementation perspective splitting tables added more complexity due to breaking up existing tables. However, this complexity was only temporary and was expected to subside with the migration of old tables. The benefit of this approach was that it reflected the true nature of our data tables. Previously one state tax had one rate and now one tax had many which was nicely reflected in &lt;code&gt;state_taxes&lt;/code&gt; and &lt;code&gt;state_tax_changes&lt;/code&gt; table.&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;This project was not easy or smooth by easy means as we had to deal with some issues that were not directly related to not have temporality but as we moved ahead with the system the choice of undertaking a large refactor proved to be correct. It was a great reminder that no matter how good you are design is, if it isn’t compatible with business it can’t get you very far.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>database</category>
      <category>postgres</category>
      <category>sql</category>
    </item>
    <item>
      <title>Challenges of Time-Based Systems Without Proper Database Structures</title>
      <dc:creator>Umair Abid</dc:creator>
      <pubDate>Wed, 06 Sep 2023 00:00:00 +0000</pubDate>
      <link>https://forem.com/umairabid/challenges-of-time-based-systems-without-proper-database-structures-3o8b</link>
      <guid>https://forem.com/umairabid/challenges-of-time-based-systems-without-proper-database-structures-3o8b</guid>
      <description>&lt;p&gt;When we store information in our database, we normally store it without a time dimension even if it is only valid for a specific period of time. For example, people move around all the time, but most apps ask for your current address and rely on you to change it whenever you move. This works because most applications have no use case to be aware of your address history and only need your current address.&lt;/p&gt;

&lt;p&gt;However, for some systems, the time dimension is omnipresent whenever data is queried or mutated, and implementing them on traditional data models can pose serious challenges. I had a chance to work on a project with similar challenges that provided a good learning experience on how to overcome them. The project makes a good use case of how temporality can help streamline operations. To go into the details while not revealing proprietary information, let’s use an example of a tax system.&lt;/p&gt;

&lt;h1&gt;
  
  
  Situation
&lt;/h1&gt;

&lt;p&gt;To understand the challenges, let’s start with an overview of the tax system. We first define some use cases for our hypothetical tax system, understand the structure of tables involved in recording tax returns for users, and deep dive into problems due to that structure.&lt;/p&gt;

&lt;h1&gt;
  
  
  Overview of Tax System
&lt;/h1&gt;

&lt;p&gt;The tax system is a single tool for residents of a country to submit their tax returns according to the tax percentages set at the state level. The system is used by two roles: administrators and taxpayers. To avoid confusion, please refrain from comparing this system to a real-world tax system, as it serves only as a reflection of the actual system we worked with. Our hypothetical tax system only supports the following use cases.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fumairabid.com%2Fassets%2Fimg%2Fp2-image-1.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fumairabid.com%2Fassets%2Fimg%2Fp2-image-1.svg" alt="Temporal System Use Case Diagram" width="556" height="591"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Taxpayers, when they sign up, enroll themselves in tax types like income tax, capital gains tax, etc. Then each year, the system calculates the amount of tax that is due for that tax year and also allows them to enter the tax they paid throughout the year. For the sake of simplicity, how those two values, i.e., tax paid and tax due, are balanced is not our concern.&lt;/p&gt;

&lt;h2&gt;
  
  
  Structure of Critical Tables
&lt;/h2&gt;

&lt;p&gt;Although the problems spanned multiple tables, they can be generalized using two tables used for storing state taxes and tax returns. The &lt;code&gt;state_taxes&lt;/code&gt; table stores the &lt;code&gt;rate&lt;/code&gt; to calculate the tax due for the taxpayer. For example, if the income tax rate is 0.7 and taxpayer income is 100$ then the income tax due is 100 * 0.07 = $7. The rate varies by type of tax and state.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fumairabid.com%2Fassets%2Fimg%2Fp2-image-2.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fumairabid.com%2Fassets%2Fimg%2Fp2-image-2.svg" alt="Temporal System Database Design" width="531" height="291"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One important thing to point out here is that the system was not designed to handle varying versions of data over time, although we have the column &lt;code&gt;year&lt;/code&gt; in the table &lt;code&gt;state_taxes&lt;/code&gt;. The access patterns assumed one row per tax for a state and the type of tax when the table is joined or read directly. In other words, there is a &lt;code&gt;unique(state_id, type)&lt;/code&gt; constraint on the table. That essentially means you cannot add the same tax for the same type, for different years. To have some audit compatibilities rows were not updated, rather updates were applied by soft deleting the old and creating new rows with updates.&lt;/p&gt;

&lt;p&gt;The other table to consider is &lt;code&gt;tax_returns&lt;/code&gt; responsible for storing the tax returns of a specific taxpayer. The table has one row per tax type for each payer, it stores tax returns within that row in the form of a JSON array.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fumairabid.com%2Fassets%2Fimg%2Fp2-image-3.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fumairabid.com%2Fassets%2Fimg%2Fp2-image-3.svg" alt="Temporal System Database Design" width="531" height="331"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;returns&lt;/code&gt; column was added as a solution for storing returns for each user while still conforming to having only one row per tax. The &lt;code&gt;deleted_at&lt;/code&gt; key served the same purpose for each JSON object as it did in &lt;code&gt;state_taxes&lt;/code&gt; the table.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problems with the Underlying Structure
&lt;/h2&gt;

&lt;p&gt;The above structure functioned correctly only when data was added in a linear time order. However, a single retroactive update, whether to correct a mistake or add a new record, could introduce data inconsistencies. These inconsistencies sometimes led to data corruption, while in other cases, data loss occurred.&lt;/p&gt;

&lt;h3&gt;
  
  
  Data Loss on Updates
&lt;/h3&gt;

&lt;p&gt;Unlike the &lt;code&gt;returns&lt;/code&gt; column in &lt;code&gt;tax_returns&lt;/code&gt;, the &lt;code&gt;state_taxes&lt;/code&gt; table lacks a JSON column to store tax rates per year, presumably due to the absence of a use case for displaying rates for each tax year. As a result, any rate update, whether for correction or addition, results in the removal of the previous rate. In cases of retroactive updates, the system effectively loses the currently effective rate.&lt;/p&gt;

&lt;p&gt;For example, suppose admin has added rates for tax years 2021 and 2023 (currently effective). They later realized that the rate for 2021 was incorrect and wanted to update it. Now since &lt;code&gt;state_taxes&lt;/code&gt; can only support one row for a tax, adding a correct rate for 2021 will result in a loss of the 2023 rate. Another case is that rates were added correctly for years 2021 and 2023 but they missed adding a rate for 2022, now adding that rate will again overwrite the rate for 2023.&lt;/p&gt;

&lt;h3&gt;
  
  
  Data Corruption on Updates
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;tax_due&lt;/code&gt; in the &lt;code&gt;results&lt;/code&gt; column of &lt;code&gt;tax_returns&lt;/code&gt; is a dynamic value calculated based on existing data in the system i.e. &lt;code&gt;income * tax_ratio&lt;/code&gt;. Normally, such a calculated value shouldn’t be stored, but due to the data loss issue mentioned earlier, it was necessary to save it to preserve the value using the tax rate effective at the time of calculation. However, this would be more akin to keeping the best possible value rather than the correct value.&lt;/p&gt;

&lt;p&gt;The value stored at the time of adding tax returns remains valid as long as the factors used for its calculation, such as the tax ratio and income, are not updated. If these factors are updated, the field will contain an incorrect value according to the current system data and cannot be verified. In some cases, it might be argued that having no value stored is preferable to having an outdated or unverifiable one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ineffective auditing capabilities
&lt;/h3&gt;

&lt;p&gt;The system is frequently used &lt;code&gt;deleted_at&lt;/code&gt; and soft deletes to prevent loss of information for auditing purposes. Since they were system level, not application level construct, they were quite ineffective in providing any help to address the problems we have seen so far, when retroactive changes were made. The best case scenario was using them to figure out if a version of data existed at some point in some system and that is it.&lt;/p&gt;

&lt;p&gt;In temporal systems, auditing capability is required at the application level to facilitate resolving risks. For example, let’s say you bought a pair of shoes. After selling you that pair, the shop realized that the price was entered incorrectly in the system and they fixed it. Now, if you go back to return the shoes if they have a proper temporal system, they can quickly find out the effective price of shoes on the date when they were sold to you. Otherwise, there is no way for the system to find out the price on the date when the shoes were sold.&lt;/p&gt;

&lt;h1&gt;
  
  
  Expectations from the Temporal System
&lt;/h1&gt;

&lt;p&gt;What we went through while trying to uncover the problems were basically a consequence of implementing time-based systems without a proper structure to support temporal transactions. This now leads us to define expectations for a temporal system to avoid the problems that we uncovered while also making it easier for users to work with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Consistent Timelines
&lt;/h2&gt;

&lt;p&gt;As we have observed, when data validity is time-dependent, it results in multiple versions of data corresponding to different points in time. These variations collectively form timelines, and it is essential to maintain their consistency. Overlapping timelines can lead to indeterministic outcomes when attempting to identify a valid record for a specific date. To address this issue, consider the following example using the &lt;code&gt;state_taxes&lt;/code&gt; table, which employs an &lt;a href="https://en.wikipedia.org/wiki/Effective_date?ref=umairabid.com" rel="noopener noreferrer"&gt;effective date range&lt;/a&gt; to denote the validity of tax rates.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fumairabid.com%2Fassets%2Fimg%2Fp2-image-4.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fumairabid.com%2Fassets%2Fimg%2Fp2-image-4.svg" alt="Temporal System Database Design" width="511" height="121"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;[start_time, end_time) is a convention to define ranges with start and end date. Here “[” means range includes start_time and “)” excludes end_time&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now, let’s consider the scenario where we need to determine the income tax rate effective on the date 2023-01-15. Upon inspecting the date ranges, we can identify that this date falls within the row with id=1. In this case, obtaining a single row ensures determinism.&lt;/p&gt;

&lt;p&gt;However, if we attempt to find the rate for any date within February 2023, we would retrieve two rows. Consequently, for this rate, it becomes impossible to ascertain which ratio to apply. The motivation behind enforcing consistent timeframes is precisely to prevent such situations from arising.&lt;/p&gt;

&lt;h2&gt;
  
  
  Consistent Implementation across tables
&lt;/h2&gt;

&lt;p&gt;The implementation of temporal tables can vary from one table to another, and there may be situations where such customization is necessary. However, in most cases, it is not the ideal approach.&lt;/p&gt;

&lt;p&gt;For instance, consider a scenario where you need to join three temporal tables together, and each of these tables has implemented temporality differently. In such cases, fetching data in a single query can be challenging, if not entirely impossible.&lt;/p&gt;

&lt;p&gt;Moreover, while it may still be feasible to write data in such a setup, doing so often means sacrificing the potential for abstraction in both read and write patterns. A consistent implementation approach, on the other hand, enables seamless integration with Object-Relational Mapping (ORM) systems, making working with temporal tables a much more straightforward and efficient process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prevent the loss of information
&lt;/h2&gt;

&lt;p&gt;One of the fundamental reasons for incorporating a temporal aspect into your data is the preservation of information. In cases where information undergoes retroactive changes, it’s crucial that the system retains the data as it existed before the alteration to maintain auditing capabilities.&lt;/p&gt;

&lt;p&gt;In monetary systems, calculations often depend on specific configurations, even if those configurations are initially incorrect. These incorrect configurations are utilized in calculations until corrected. When these configurations are rectified later, with their effective or validity period remaining the same but only the data being updated, the system is still expected to retain the original configurations. They can help with auditing when you need to check what was calculated before at a specific point in time.&lt;/p&gt;

&lt;h1&gt;
  
  
  Solution
&lt;/h1&gt;

&lt;p&gt;As you might have already discerned, while many of these challenges and expectations can be addressed by extending the current design, such as expanding JSON columns to cover other columns and implementing upsert hooks to maintain system consistency, it’s evident that straightforward use cases can rapidly escalate the complexity of a system.&lt;/p&gt;

&lt;p&gt;In our forthcoming article, we will delve into a solution that tackles these issues without unnecessarily inflating system complexity.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>database</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Automation Engine Refactor for Performance and Maintainability</title>
      <dc:creator>Umair Abid</dc:creator>
      <pubDate>Mon, 07 Aug 2023 00:00:00 +0000</pubDate>
      <link>https://forem.com/umairabid/automation-engine-refactor-for-performance-and-maintainability-cci</link>
      <guid>https://forem.com/umairabid/automation-engine-refactor-for-performance-and-maintainability-cci</guid>
      <description>&lt;p&gt;Imagine starting your day with your mailbox full of outages due to all database connections being held up for an extensive period. Nobody likes it and our team went on a mission to ensure we never have such a day again, at least for the exact root cause.&lt;/p&gt;

&lt;h1&gt;
  
  
  Situation
&lt;/h1&gt;

&lt;p&gt;The problem originated from the Pipeline Automation Engine of our CRM app. A pipeline consists of a series of stages that a &lt;a href="https://en.wikipedia.org/wiki/Lead_generation?ref=umairabid.com" rel="noopener noreferrer"&gt;lead&lt;/a&gt; go through to either become a sale or be lost. Each stage has associated actions like sending emails or text, in addition to the move action which decides the next stage for the lead. To understand how the flow works, please consider the preliminary database design below.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fumairabid.com%2Fassets%2Fimg%2Fp1-image-1.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fumairabid.com%2Fassets%2Fimg%2Fp1-image-1.svg" alt="Automation Engine Database Design" width="476" height="342"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The right side of the design is relations or tables containing the configuration which dictates how automation will be executed. Whereas the left side helps run the pipeline automation for the specific lead. Here is a brief summary of each table,&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pipeline&lt;/strong&gt; : For example “Google Adwords Campaign”, can be one pipeline to convert leads from google adwords campaign to sales.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pipelines Stages&lt;/strong&gt; : Contains stages for each pipeline, for example, “Inquired”, “Responded” etc can be the stages that a lead goes through.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pipeline Stage Actions&lt;/strong&gt; : Send an introduction email and then move them to the “responded” stage would be an example of how actions work together, where sending an email and moving them to the stage are separate actions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lead&lt;/strong&gt; : Any internet user who clicked on your ad, landed on your page, and gave their information.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lead Stages&lt;/strong&gt; : Contains all the stages a lead has been or is currently in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lead Stage Actions&lt;/strong&gt; : All the actions which have been performed on lead are recorded by stage in this table. As soon as the lead enters in stage, this table is also populated with actions for that stage. Actions are executed serially.&lt;/p&gt;

&lt;h1&gt;
  
  
  Problem
&lt;/h1&gt;

&lt;p&gt;The beauty of startups is that you build something for one purpose and customers may use it in all different ways except the one it was intended for. This automation feature was built to manage the lead automation coming from landing pages, but one of our customers imported around 16k leads and ran automation on all of them. This caused an instant outage, where the connections were held up by queries coming from the automation system code. When we investigated the code scheduled to run after every five minutes, the problem became very apparent. Below is the simplified version of that code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;each&lt;/span&gt; &lt;span class="n"&gt;lead&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;leads&lt;/span&gt;
  &lt;span class="n"&gt;lead_stages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_lead_stages&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;each&lt;/span&gt; &lt;span class="n"&gt;lead_stage&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;lead_stages&lt;/span&gt;
    &lt;span class="n"&gt;last_performed_action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lead_stage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;last_performed_action&lt;/span&gt;
    &lt;span class="n"&gt;sequence_number&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;last_performed_action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sequence_number&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;action_to_perform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lead_stage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pipleline_stage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;action_after&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sequence_number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;action_to_perform&lt;/span&gt;
        &lt;span class="n"&gt;action_to_perform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perform&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The thing which instantly comes out and explains the problem is that we are querying the full leads table after every five minutes, some non-apparent problems which were adding fuel to the fire were,&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The job had no unique clause or any preventive measures to not schedule the job if the previously scheduled was still running&lt;/li&gt;
&lt;li&gt;No eager loading is being used&lt;/li&gt;
&lt;li&gt;Truly brute force, not making any use of information already stored in the system to determine which leads and actions need to be performed. Hence too many unnecessary computations.&lt;/li&gt;
&lt;/ol&gt;

&lt;h1&gt;
  
  
  Solution
&lt;/h1&gt;

&lt;p&gt;The brute-force nature of the solution provided an obvious hint for the solution i.e. limit unnecessary computations. Considering the major source of unnecessary computations was scanning the leads table, we could also rephrase the problem to “How do we only fetch the leads which have pending stage actions”. Once the problem was stated, the solution was a no-brainer since we can easily filter out the leads for whom all stage actions have been executed.&lt;/p&gt;

&lt;p&gt;Couple the above improvement which significantly reduced the leads every time the job is run with improvement over making the job unique and not scheduling it if the is still in progress, the two quick fixes helped us to resolve the outage, but we had to ask how long until the next outage?&lt;/p&gt;

&lt;h1&gt;
  
  
  Challenges on the Horizon
&lt;/h1&gt;

&lt;p&gt;This was one of the core features where performance was not only expected but needed to be guaranteed under specific SLAs (e.g. the next action should be performed within 2 minutes after performing the previous one). Considering how one customer used the system in a way it was not intended to be used, it was only a matter of time before other customers put the system under identical stress. The system had to be rethought and replanned to at least give the first few hundred customers the best experience while we invested in other parts of the app.&lt;/p&gt;

&lt;p&gt;After a few discussions and meetings, the following problems (in order of their priority) were identified to be fixed,&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;One lead action should not block another lead action. Sending emails or texts can be an expensive operations&lt;/li&gt;
&lt;li&gt;Performing actions can fail due to any number of reasons and the system is missing the retry ability. This especially becomes important due to the rate limits of third-party services. This also aggregates the first problem.&lt;/li&gt;
&lt;li&gt;Importing 16000 leads is fine but adding them all into automation at once is not, especially when multiple accounts do that in a narrow window of time. There should be a limit on how many automation can be scheduled per account.&lt;/li&gt;
&lt;li&gt;The query to fetch leads with pending actions might still return where no further action is required. For example 

&lt;ul&gt;
&lt;li&gt;“wait” action can be used to add a buffer between actions until the wait time is over, no action can be performed on lead&lt;/li&gt;
&lt;li&gt;Some actions might be triggered when as a response from the lead, like a reply to an email or text. Until a reply is received or the threshold to receive a reply is not over, no action can be performed on the lead.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h1&gt;
  
  
  Scaling upwards
&lt;/h1&gt;

&lt;p&gt;The first two problems pointed out that our system is missing two key pieces, making the automation loop async and applying throttling on automations by the organization. For rest, we also needed to augment &lt;code&gt;lead_stage_actions&lt;/code&gt; table to store some extra information which would help filter out the leads if they are pending on user action or just need to be scheduled at some time in the future. To work around the problems we added,&lt;/p&gt;

&lt;p&gt;Two columns in the &lt;code&gt;lead_stage_actions&lt;/code&gt; table,&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;perform_at&lt;/code&gt; Action can only be performed after this timestamp&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;status&lt;/code&gt; Hold status for lead actions, only pending actions can be performed&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;and few classes, two fundamental classes were &lt;code&gt;Scheduler&lt;/code&gt; responsible for querying and distributing actions to their appropriate handlers and &lt;code&gt;AutomationActionHandler&lt;/code&gt; which all individual action handlers extend from (e.g. &lt;code&gt;EmailActionHandler&lt;/code&gt;, &lt;code&gt;SmsActionHandler&lt;/code&gt; etc.)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fumairabid.com%2Fassets%2Fimg%2Fp1-image-2.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fumairabid.com%2Fassets%2Fimg%2Fp1-image-2.svg" alt="Automation Enginer Class Diagram" width="672" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Knitting Everything Together
&lt;/h1&gt;

&lt;p&gt;Eventually, we replaced the original automation loop with the following flow encapsulating primary automation flow end to end. The dashed lines represent the async/indirect flow where the next step is not executed in the same process. Few highlights of the flow&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The scheduler is lean and only depends on one simple query.&lt;/li&gt;
&lt;li&gt;All action handlers get executed in separate threads independently.&lt;/li&gt;
&lt;li&gt;Rate limiting is applied on the action handlers level, allowing users to add leads in stages but preventing organizations to use more than allocated processing.&lt;/li&gt;
&lt;li&gt;The automation fails gracefully in case of errors.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fumairabid.com%2Fassets%2Fimg%2Fp1-image-3.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fumairabid.com%2Fassets%2Fimg%2Fp1-image-3.svg" alt="Automation Enginer Flow Chart" width="689" height="1211"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Aftermath
&lt;/h1&gt;

&lt;p&gt;After the release, we continue monitoring the performance and user activities but nothing major came up, except tweaking limits and small bug fixes here and there. We had some concerns that relying on status to identify pending action may run into concurrency issues but since the application was not supposed to run on a massive scale just yet, we relied on database locks to ensure consistency.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>automation</category>
      <category>backend</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
