<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Josephat Macharia</title>
    <description>The latest articles on DEV Community by Josephat Macharia (@mashaa).</description>
    <link>https://dev.to/mashaa</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F432874%2F0f9b169a-46a0-43a6-9371-136b7831351e.jpeg</url>
      <title>DEV Community: Josephat Macharia</title>
      <link>https://dev.to/mashaa</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mashaa"/>
    <language>en</language>
    <item>
      <title>No cloud to lean on: building a reliable data platform on a bank's own servers</title>
      <dc:creator>Josephat Macharia</dc:creator>
      <pubDate>Sun, 01 Nov 2026 08:35:07 +0000</pubDate>
      <link>https://dev.to/mashaa/no-cloud-to-lean-on-building-a-reliable-data-platform-on-a-banks-own-servers-2i2k</link>
      <guid>https://dev.to/mashaa/no-cloud-to-lean-on-building-a-reliable-data-platform-on-a-banks-own-servers-2i2k</guid>
      <description>&lt;p&gt;Replication, failover, graceful shutdown, autoscaling and alerting usually arrive as managed services. Inside a bank, you build them from Kubernetes, an operator or two and a lot of small decisions. Here is how each one works, with the code.&lt;/p&gt;

&lt;p&gt;Field notes from moving a transaction reconciliation platform from AWS onto Kubernetes inside banks' data centres&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The constraints&lt;/li&gt;
&lt;li&gt;Five words, one goal&lt;/li&gt;
&lt;li&gt;Reliability: no work is lost silently&lt;/li&gt;
&lt;li&gt;Fault tolerance: replication, redundancy, failover, graceful degradation&lt;/li&gt;
&lt;li&gt;High availability: what survives what&lt;/li&gt;
&lt;li&gt;Scalability on a fixed number of servers&lt;/li&gt;
&lt;li&gt;Monitoring and observability without the internet&lt;/li&gt;
&lt;li&gt;High availability is not a backup&lt;/li&gt;
&lt;li&gt;Checklist&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The constraints
&lt;/h2&gt;

&lt;p&gt;In the cloud, our reconciliation platform ran on managed pieces: a managed database with automatic failover, managed queues, managed object storage, autoscaling groups and a monitoring SaaS. Banks wanted the same platform on their own hardware and their hardware comes with rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No internet.&lt;/strong&gt; Images come from the bank's registry, backups go to the bank's storage and the only way out is the bank's mail relay.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A fixed number of servers.&lt;/strong&gt; Three to seven machines, sized months in advance. Nothing appears when load goes up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No managed services.&lt;/strong&gt; Every database, queue and object store runs inside the cluster, so failover is our problem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Little access.&lt;/strong&gt; The people on call may not have cluster access and our engineers get in only through a bastion, when the bank allows it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The platform itself is a typical event-driven data system. Users upload files through a web portal. Workers clean them, an ingestion engine loads them into an Iceberg data lake with Spark and reconciliations match transactions across sources. Every hop between services goes through a message queue.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fycpnpbc4wlpn6kanujni.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fycpnpbc4wlpn6kanujni.png" alt=" " width="800" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Figure 1.&lt;/strong&gt; The platform inside one Kubernetes cluster. The only things that leave are backups, email and image pulls and each goes to a system the bank controls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five words, one goal
&lt;/h2&gt;

&lt;p&gt;These terms get used loosely, so here is what each one meant for us. They overlap and they all serve one goal we wrote at the top of the design: &lt;strong&gt;losing any one machine loses no data and stops nothing for more than seconds.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;The question it answers&lt;/th&gt;
&lt;th&gt;What it looks like here&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Reliability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Does the system do the right thing, every time?&lt;/td&gt;
&lt;td&gt;Work is acknowledged only after it is done; locks can't be taken twice; shutdown never drops a file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Fault tolerance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;What happens when a part fails?&lt;/td&gt;
&lt;td&gt;Replicated database and queues, spread replicas, automatic failover, failures contained to one job&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;High availability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;How long are users affected?&lt;/td&gt;
&lt;td&gt;Seconds for a lost server; zero for upgrades and node maintenance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Scalability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Can it keep up when the work grows?&lt;/td&gt;
&lt;td&gt;Replica counts follow queue depth; heavy jobs wait for room instead of failing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Observability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Will someone find out and can they tell why?&lt;/td&gt;
&lt;td&gt;Alerts by email, searchable logs, dashboards, a daily health report&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Reliability: no work is lost silently
&lt;/h2&gt;

&lt;p&gt;The rule underneath everything else: &lt;strong&gt;work moves through the queue and a message leaves the queue only when its work is done.&lt;/strong&gt; A service that is down or busy leaves messages waiting. Nothing is lost while it recovers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Acknowledge after the work, never before
&lt;/h3&gt;

&lt;p&gt;A message broker like RabbitMQ hands a message to one consumer and marks it &lt;em&gt;unacknowledged&lt;/em&gt;. It stays in the queue until the consumer says what happened. That gives you three outcomes and each one needs a deliberate answer.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa73tf29cj7xadocaugzg.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa73tf29cj7xadocaugzg.png" alt=" " width="800" height="274"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Figure 2.&lt;/strong&gt; Every path out of "unacknowledged" is deliberate. The message disappears only on success; everything else either retries or lands somewhere a person will see it.&lt;/p&gt;

&lt;p&gt;Here is the pattern in a small consumer that forwards events from a queue to an HTTP endpoint. It acknowledges only after the endpoint answers 2xx and on any failure it puts the message back for another attempt:&lt;/p&gt;

&lt;p&gt;Python (kombu)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def forwarder(endpoint, http_timeout):
    def on_message(body, message):
        try:
            response = requests.post(endpoint, data=to_payload(body), timeout=http_timeout)
            response.raise_for_status()
            message.ack()                      # only now does it leave the queue
        except Exception:
            logger.exception("Failed to forward message; requeueing")
            message.reject(requeue=True)       # back to Ready, tried again
    return on_message
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two settings make that safe at the broker. Quorum queues in RabbitMQ drop a message after 20 failed deliveries by default. A consumer that requeues on purpose while its downstream is down (the portal restarting, say) would lose messages after a few minutes of retries, so we lift the limit with a policy and rely on dead-letter queues and alerts for work that truly can't succeed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl -X PUT "$api/policies/%2F/redelivery" -H 'content-type: application/json' \
  -d '{"pattern": ".*", "apply-to": "quorum_queues",
       "definition": {"delivery-limit": -1}, "priority": 0}'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Watch out for the poison message.&lt;/strong&gt; Unlimited redelivery has a dark side. A message whose processing kills the consumer is redelivered to the next consumer, which dies too.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fde69ytooiwjss0o6ga36.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fde69ytooiwjss0o6ga36.png" alt=" " width="800" height="179"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Figure 3.&lt;/strong&gt; The poison loop we hit on a test server. One large file was redelivered from pod to pod, killing each one at its memory limit.&lt;/p&gt;

&lt;p&gt;The fix was sizing, not code: the file cleaner's memory limit went from 2 GiB to 4 GiB and a day later the pods still had no restarts. The lessons are general, though:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Size memory limits for the largest input, not the average one.&lt;/strong&gt; An OOM kill is instant: no exception handler runs, nothing is logged by the app and the message goes back to the queue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alert on OOM kills and on restarts&lt;/strong&gt;, so a loop shows up in minutes rather than as "the system is slow today".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stream large inputs&lt;/strong&gt; where you can, so memory stays flat whatever the file size.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Consumers that can't stall silently
&lt;/h3&gt;

&lt;p&gt;A web service has an obvious health check: send it a request. A queue consumer has nothing to probe and its worst failure leaves the process running. The connection drops, the consume loop blocks forever, the pod shows &lt;code&gt;Running&lt;/code&gt; and the queue grows behind it.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5syb8mn9tkwso3kj8iw7.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5syb8mn9tkwso3kj8iw7.png" alt=" " width="800" height="263"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Figure 4.&lt;/strong&gt; A heartbeat written only by the loop that does the work turns "running but useless" into a restart.&lt;/p&gt;

&lt;p&gt;The consumer touches a file at the end of each pass through its loop. Long jobs run on their own threads, so a big file never holds the loop up and the heartbeat really does mean "this process is still taking work":&lt;/p&gt;

&lt;p&gt;Python&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LIVENESS_FILE = pathlib.Path("/tmp/heartbeat")

while not stopping:
    try:
        conn.drain_events(timeout=5)
    except socket.timeout:
        pass
    conn.heartbeat_check()
    LIVENESS_FILE.touch()          # only a working loop gets here
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kubernetes checks the file's age. A startup probe gives the process time for its first beat; after that, three stale checks in a row restart it:&lt;/p&gt;

&lt;p&gt;Kubernetes&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;startupProbe:
  exec:
    command: ["sh", "-c", 'f=/tmp/heartbeat; [ -f "$f" ] &amp;amp;&amp;amp; [ $(( $(date +%s) - $(stat -c %Y "$f") )) -lt 120 ]']
  periodSeconds: 10
  failureThreshold: 30          # up to 5 minutes for the first heartbeat
livenessProbe:
  exec:
    command: ["sh", "-c", 'f=/tmp/heartbeat; [ -f "$f" ] &amp;amp;&amp;amp; [ $(( $(date +%s) - $(stat -c %Y "$f") )) -lt 120 ]']
  periodSeconds: 30
  failureThreshold: 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Probes can cause outages too. RabbitMQ's usual readiness check, &lt;code&gt;rabbitmq-diagnostics ping&lt;/code&gt;, starts a whole separate Erlang runtime to answer. On a busy node that timed out while the broker was healthy, which pulled it out of the Service and refused every client. A plain &lt;code&gt;tcpSocket&lt;/code&gt; check is a kernel-level connect with no process to start and it stays reliable under the same load.&lt;/p&gt;

&lt;h3&gt;
  
  
  Locks that can't be taken twice
&lt;/h3&gt;

&lt;p&gt;Two reconciliations on the same account at the same time would write conflicting results, so each run locks its accounts first. The obvious way ("check whether a lock exists, then write one") lets two triggers that arrive together both see "unlocked". The lock has to be a &lt;strong&gt;single conditional write&lt;/strong&gt; and it has to record who owns it:&lt;/p&gt;

&lt;p&gt;SQL (illustrative)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-- Take the lock only if nobody holds a live one. Exactly one caller gets a row back.
INSERT INTO account_locks (account_id, owner, expires_at)
VALUES (:account, :owner, now() + interval '1 hour')
ON CONFLICT (account_id) DO UPDATE
  SET owner = EXCLUDED.owner, expires_at = EXCLUDED.expires_at
  WHERE account_locks.expires_at &amp;lt; now()          -- take over only a stale lock
RETURNING account_id;

-- Release only your own lock, never someone else's.
DELETE FROM account_locks WHERE account_id = :account AND owner = :owner;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lock several accounts in a fixed order&lt;/strong&gt; and on a conflict release the ones already taken. Two runs grabbing the same accounts in different orders is the classic deadlock.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every lock-holding workflow releases its locks on failure&lt;/strong&gt;, on every step. Otherwise a failed run blocks the account until the lock expires.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the lock table in the replicated, backed-up database.&lt;/strong&gt; We moved this state from DynamoDB in the cloud to a Postgres table behind the same interface. Adding a separate store would have meant one more thing to make highly available and back up.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Things that must happen once
&lt;/h3&gt;

&lt;p&gt;Some work must never run twice at the same moment and Kubernetes happily starts several copies of everything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Database migrations.&lt;/strong&gt; On a first install, or when scaling up from zero, every portal pod starts together and concurrent &lt;code&gt;migrate&lt;/code&gt; runs on an empty database fail on duplicate keys. Each pod's init container takes a Postgres advisory lock first. One pod migrates and the others wait and then find nothing left to do:&lt;/p&gt;

&lt;p&gt;Python (init container)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import django
django.setup()
from django.core.management import call_command
from django.db import connection

with connection.cursor() as cursor:
    # Held by this session until the process exits.
    cursor.execute("SELECT pg_advisory_lock(hashtext('portal-migrate'))")
call_command("migrate", interactive=False)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The scheduler.&lt;/strong&gt; Two copies of a periodic-task scheduler fire every task twice. It runs as one replica with the &lt;code&gt;Recreate&lt;/code&gt; strategy, so even an upgrade stops the old pod before starting the new one. Everything else rolls with a surge, so a new pod is ready before an old one goes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# scheduler: never two at once
strategy:
  type: Recreate

# portal: never fewer than the desired count during a rollout
strategy:
  type: RollingUpdate
  rollingUpdate: { maxSurge: 1, maxUnavailable: 0 }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Graceful shutdown: finish the work in hand
&lt;/h3&gt;

&lt;p&gt;Pods are stopped all the time: every upgrade, node patch and scale-down. Kubernetes sends &lt;code&gt;SIGTERM&lt;/code&gt;, waits for the grace period, then kills the process. A worker that ignores &lt;code&gt;SIGTERM&lt;/code&gt; loses whatever it was doing on every deployment.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F02z6t6u8qb76tof8ybwm.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F02z6t6u8qb76tof8ybwm.png" alt=" " width="800" height="232"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Figure 5.&lt;/strong&gt; Graceful shutdown has two good endings. Either the work finishes and is acknowledged, or it is left unacknowledged and redone elsewhere. The bad ending, a half-finished file marked as done, can't happen.&lt;/p&gt;

&lt;p&gt;Python&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;stopping = False

def on_sigterm(signum, frame):
    global stopping
    stopping = True                  # finish the current message, then leave

signal.signal(signal.SIGTERM, on_sigterm)

with Connection(broker_url, heartbeat=60) as conn, conn.Consumer(queues, callbacks=[on_message]):
    while not stopping:
        try:
            conn.drain_events(timeout=5)   # returns at least every 5s to check the flag
        except socket.timeout:
            pass
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details are easy to miss:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;exec&lt;/code&gt; the real process&lt;/strong&gt; in a shell entrypoint (&lt;code&gt;exec celery worker ...&lt;/code&gt;). Otherwise &lt;code&gt;SIGTERM&lt;/code&gt; goes to the shell, which ignores it and the worker is killed at the end of the grace period with no warning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Know when your framework acknowledges.&lt;/strong&gt; Celery acknowledges a task when it &lt;em&gt;starts&lt;/em&gt; by default, so a task cut off by &lt;code&gt;SIGKILL&lt;/code&gt; is gone. &lt;code&gt;acks_late&lt;/code&gt; fixes that, but then a task runs again after a crash, which is only safe when every task can run twice. We left it off until every task is idempotent.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;On SIGTERM&lt;/th&gt;
&lt;th&gt;Grace period&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;File and report workers&lt;/td&gt;
&lt;td&gt;Stop consuming, finish and acknowledge the job in hand&lt;/td&gt;
&lt;td&gt;900s / 1,800s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Celery workers&lt;/td&gt;
&lt;td&gt;Stop taking tasks, finish running ones&lt;/td&gt;
&lt;td&gt;900s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ingestion engine&lt;/td&gt;
&lt;td&gt;A &lt;code&gt;preStop&lt;/code&gt; drain: stop picking up runs, wait for its own to finish&lt;/td&gt;
&lt;td&gt;1,800s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Event forwarder, notifier&lt;/td&gt;
&lt;td&gt;Finish the current message or email&lt;/td&gt;
&lt;td&gt;60s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Message broker node&lt;/td&gt;
&lt;td&gt;Wait until every queue keeps a majority without it (see failover below)&lt;/td&gt;
&lt;td&gt;Up to the grace period&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQL engine worker&lt;/td&gt;
&lt;td&gt;Finish running query tasks&lt;/td&gt;
&lt;td&gt;Until its tasks end&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Fault tolerance
&lt;/h2&gt;

&lt;p&gt;Fault tolerance is four techniques working together. &lt;strong&gt;Replication&lt;/strong&gt; keeps the data in more than one place. &lt;strong&gt;Redundancy&lt;/strong&gt; keeps more than one copy of each service running, on different machines. &lt;strong&gt;Failover&lt;/strong&gt; moves the work to a healthy copy when one dies. &lt;strong&gt;Graceful degradation&lt;/strong&gt; keeps the rest working, more slowly if it must, when something can't be replaced.&lt;/p&gt;

&lt;h3&gt;
  
  
  Replication: the database
&lt;/h3&gt;

&lt;p&gt;Postgres runs as a three-instance cluster managed by the &lt;a href="https://cloudnative-pg.io" rel="noopener noreferrer"&gt;CloudNativePG&lt;/a&gt; operator: one primary and two streaming replicas, each on a different server. Every service connects to one Service name (&lt;code&gt;postgres-rw&lt;/code&gt;) and the operator always points it at the current primary.&lt;/p&gt;

&lt;p&gt;We chose it because it uses the Kubernetes API itself to decide which instance is primary. There is no extra consensus store (Patroni-based setups need etcd or similar) and no failover scripts of our own. WAL archiving and point-in-time recovery are built in, it's a CNCF project and it's on OpenShift's OperatorHub, which matters to banks that run OpenShift.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk7v7s5k4q2d0oe147zrk.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk7v7s5k4q2d0oe147zrk.png" alt=" " width="799" height="284"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Figure 6.&lt;/strong&gt; Synchronous, quorum-style replication. A commit the application was told about survives the loss of any one server.&lt;/p&gt;

&lt;p&gt;CloudNativePG Cluster&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: postgres
spec:
  instances: 3
  primaryUpdateStrategy: unsupervised   # roll replicas, then switch over, no human needed
  smartShutdownTimeout: 15              # default 180s: see below
  enablePDB: true
  affinity:
    enablePodAntiAffinity: true         # one instance per server
  postgresql:
    synchronous:
      method: any                       # synchronous_standby_names = 'ANY 1 (...)'
      number: 1
      dataDurability: preferred         # degrade to async rather than stop writes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;preferred&lt;/code&gt; rather than &lt;code&gt;required&lt;/code&gt; is a graceful-degradation choice. With &lt;code&gt;required&lt;/code&gt;, every write stops while no replica is up and a rolling update or a lost server would stop the platform. The window where only one copy exists is short and an alert says when it opens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One default worth changing:&lt;/strong&gt; &lt;code&gt;smartShutdownTimeout&lt;/code&gt;. A stopping primary waits for open connections to close before a replica takes over. Django keeps its database connections open, so the default 180 seconds meant three minutes of errors on every primary restart. Fifteen seconds is plenty.&lt;/p&gt;

&lt;h3&gt;
  
  
  Replication: the message queues
&lt;/h3&gt;

&lt;p&gt;RabbitMQ runs as a three-node cluster with &lt;strong&gt;quorum queues&lt;/strong&gt;: each queue is a Raft group copied on all three nodes. A message is confirmed to the publisher once a majority has it, so the queue keeps working and loses no confirmed message, while any two nodes are up.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiauh93e7xxjn3j804xw0.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiauh93e7xxjn3j804xw0.png" alt=" " width="800" height="221"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Figure 7.&lt;/strong&gt; A quorum queue tolerates one lost node out of three. Two lost nodes stop it, by design, rather than risk two diverging copies.&lt;/p&gt;

&lt;p&gt;rabbitmq.conf&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Every node finds the others through the Kubernetes API.
cluster_formation.peer_discovery_backend = k8s
cluster_formation.k8s.address_type = hostname

# Every durable queue a client declares without a type becomes a quorum queue,
# so no client code has to change.
default_queue_type = quorum

# A queue declared while the cluster was still forming gains replicas as nodes join.
quorum_queue.continuous_membership_reconciliation.enabled = true
quorum_queue.continuous_membership_reconciliation.target_group_size = 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Setting the default at the broker was the important part. Every service that declares queues kept its code unchanged. Celery was the one exception. Without declaring quorum queues itself, it asks for a global QoS setting that quorum queues reject and it can't consume at all. One setting fixed it: &lt;code&gt;CELERY_QUORUM_QUEUES&lt;/code&gt;, read from the environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Redundancy: copies on different machines
&lt;/h3&gt;

&lt;p&gt;Three copies of a service on one server are one failure away from zero. Every workload with more than one replica &lt;strong&gt;prefers to spread&lt;/strong&gt; across servers and has a &lt;strong&gt;PodDisruptionBudget&lt;/strong&gt;, so a node drain (patching, a hardware swap) can't take every copy at once.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkjd85s5njdj9dlaryydm.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkjd85s5njdj9dlaryydm.png" alt=" " width="800" height="263"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Figure 8.&lt;/strong&gt; Spreading puts each copy on a different server. The disruption budget makes a drain wait whenever taking the next pod would leave too few copies.&lt;/p&gt;

&lt;p&gt;Both come from two small Helm helpers that every Deployment includes:&lt;/p&gt;

&lt;p&gt;Helm helpers&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{{- define "chart.spreadReplicas" -}}
affinity:
  podAntiAffinity:
    preferredDuringSchedulingIgnoredDuringExecution:   # prefer, so a 1-server install still schedules
      - weight: 100
        podAffinityTerm:
          topologyKey: kubernetes.io/hostname
          labelSelector:
            matchLabels:
              app: {{ .app }}
{{- end -}}

{{- define "chart.pdb" -}}
{{- if gt (int .replicas) 1 }}   # a PDB over one replica would block drains forever
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: {{ .app }}
spec:
  maxUnavailable: 1
  selector:
    matchLabels:
      app: {{ .app }}
{{- end }}
{{- end -}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both helpers handle the small case on purpose. Some installs are a single server for user acceptance testing. A hard anti-affinity rule would leave pods &lt;code&gt;Pending&lt;/code&gt; there and a disruption budget over one replica would make every drain hang.&lt;/p&gt;

&lt;h3&gt;
  
  
  Failover: when a copy dies
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The database&lt;/strong&gt; fails over by itself. When the primary's pod or server is lost, CloudNativePG promotes the most up-to-date replica within seconds and moves &lt;code&gt;postgres-rw&lt;/code&gt; to it. Clients see their connections drop and reconnect. With synchronous replication, no committed transaction is lost. The old primary rejoins as a replica when it comes back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The broker&lt;/strong&gt; needs care at planned shutdowns. Stopping a RabbitMQ node while another is already down would take queues below a majority. So before a node stops, a &lt;code&gt;preStop&lt;/code&gt; hook waits until every quorum queue would still have a majority without it, then moves its queue leaders away:&lt;/p&gt;

&lt;p&gt;StatefulSet container&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;terminationGracePeriodSeconds: 600
lifecycle:
  preStop:
    exec:
      # Wait for quorum without this node, then hand off its leaders.
      # Gives up before the grace period ends, so a node can still stop
      # if another one is down for good.
      command: ["/bin/sh", "-c",
        "rabbitmq-upgrade await_online_quorum_plus_one -t 540; rabbitmq-upgrade drain -t 30 || true"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Stateless workers with long-running state&lt;/strong&gt; are the hard case. The ingestion engine runs two or more pods, each running reconciliation workflows that hold account locks. If a pod dies mid-run, someone has to notice, mark its runs as failed and free its locks. Nobody else can tell a dead pod from a busy one, so each pod writes a heartbeat row and every pod checks the others':&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff7y81reo89xuhf9gknct.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff7y81reo89xuhf9gknct.png" alt=" " width="799" height="347"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Figure 9.&lt;/strong&gt; Peer failure detection with heartbeats. Any surviving pod can clean up after a dead one and it frees exactly the locks the dead pod's runs held.&lt;/p&gt;

&lt;p&gt;The subtle part is not judging others while you're the one in trouble. If the database is down for two minutes, every pod's heartbeat fails and when it returns every heartbeat looks expired. A pod therefore judges others only after its &lt;em&gt;own&lt;/em&gt; heartbeats have succeeded for a full timeout period. By then every live pod has written a fresh row too:&lt;/p&gt;

&lt;p&gt;Python&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class RunnerHeartbeat:
    def __init__(self, table, runner_id, clock=time.monotonic):
        self.table, self.runner_id, self.clock = table, runner_id, clock
        self.healthy_since = clock()

    def beat(self):
        try:
            self.table.put(runner_id=self.runner_id, expires_at=int(time.time()) + HEARTBEAT_TTL)
        except Exception:
            self.healthy_since = self.clock()      # our own view is unreliable: start over
            logger.exception("Runner heartbeat write failed")

    def may_reap(self):
        """True once our own heartbeats have succeeded for a full TTL:
        any live runner has then written a fresh row too."""
        return self.clock() - self.healthy_since &amp;gt;= HEARTBEAT_TTL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Crash and alert, don't re-run.&lt;/strong&gt; A dead pod's runs are marked Crashed, not retried automatically. A reconciliation stopped half-way may have written part of its results and running it again isn't known to be safe. Nothing is lost silently: the run shows as Crashed, the account is unlocked and a person is told. Automatic retry is a feature you earn by making the work idempotent.&lt;/p&gt;

&lt;h3&gt;
  
  
  Graceful degradation: fail small, slow down rather than fall over
&lt;/h3&gt;

&lt;p&gt;Some failures can't be hidden. The aim then is that they stay small and that overload produces waiting rather than errors.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb2u8b97kisdvkwxs4iy8.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb2u8b97kisdvkwxs4iy8.png" alt=" " width="800" height="242"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Figure 10.&lt;/strong&gt; Isolation as graceful degradation. Running each heavy job in its own pod keeps a failure the size of the job.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Heavy jobs fail alone.&lt;/strong&gt; We considered Spark standalone (driver inside the service) and Spark's own Kubernetes mode, whose &lt;code&gt;spark-submit&lt;/code&gt; exits 0 even when the job fails. A plain Kubernetes Job already gives a status, a deadline, retries, clean-up and logs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Too much work waits instead of failing.&lt;/strong&gt; Without a limit, a burst of uploads starts every Spark Job at once. Those the cluster has no room for sit &lt;code&gt;Pending&lt;/code&gt; with their deadline running and time out without having run. A queue in front of them (next section) turns a burst into a backlog.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Downstream outages become retries.&lt;/strong&gt; When the portal restarts, the event forwarder's requests fail and the messages are requeued until it's back. Users see their notifications a minute late, not never.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replication degrades to async&lt;/strong&gt; rather than stopping writes (Figure 6).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Telemetry never blocks the app.&lt;/strong&gt; Services send traces and metrics to a gateway asynchronously. If the gateway or log store is down, the services carry on and the gateway buffers what it can.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CPU limits slow a pod down; memory limits kill it.&lt;/strong&gt; That asymmetry decides how to set each one, as the scalability section explains.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  High availability: what survives what
&lt;/h2&gt;

&lt;p&gt;High availability is the sum of the above, measured from the user's side. We kept one table in the architecture docs that answers "what happens if X fails?" and every design change had to keep it true:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;If this fails&lt;/th&gt;
&lt;th&gt;What happens&lt;/th&gt;
&lt;th&gt;Users notice&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A database pod or its server&lt;/td&gt;
&lt;td&gt;A replica is promoted; no committed transaction is lost; services reconnect&lt;/td&gt;
&lt;td&gt;A few seconds of errors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A message broker node&lt;/td&gt;
&lt;td&gt;Every queue keeps a majority on the other two; no confirmed message is lost&lt;/td&gt;
&lt;td&gt;Nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A portal pod or its server&lt;/td&gt;
&lt;td&gt;The other replicas keep serving; the pod is rescheduled&lt;/td&gt;
&lt;td&gt;Nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A workflow pod mid-run&lt;/td&gt;
&lt;td&gt;Its runs are marked Crashed within 3 minutes, their locks freed, an alert sent&lt;/td&gt;
&lt;td&gt;That run shows as Crashed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A pod being stopped (upgrade, drain)&lt;/td&gt;
&lt;td&gt;It finishes its current work first&lt;/td&gt;
&lt;td&gt;Nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A heavy job&lt;/td&gt;
&lt;td&gt;Only that job fails, with its error in the run&lt;/td&gt;
&lt;td&gt;One failed upload&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The whole cluster&lt;/td&gt;
&lt;td&gt;Reinstall and restore from off-cluster backups&lt;/td&gt;
&lt;td&gt;An outage, within the agreed recovery time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The object store's disk&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Not covered by HA yet:&lt;/strong&gt; restore from the last bucket copy&lt;/td&gt;
&lt;td&gt;An outage&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That last row matters. A useful HA table lists what isn't covered, so the bank can accept it in writing or fund the fix. Ours is the object store, which still sits on a single disk. It's on the open-items list the bank signs at go-live.&lt;/p&gt;

&lt;p&gt;Upgrades are the most common "failure" of all, so they get the same treatment. Setup work runs as ordinary Jobs named after the Helm revision, not as Helm hooks. That way &lt;code&gt;helm upgrade --install --wait --atomic&lt;/code&gt; works end to end and a failed upgrade rolls itself back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;helm upgrade --install platform . -f values-bank.yaml -n platform --wait --atomic --timeout 20m
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Scalability on a fixed number of servers
&lt;/h2&gt;

&lt;p&gt;In the cloud, scaling means adding machines. A bank's cluster has the servers it has. Autoscaling there doesn't add capacity. It &lt;strong&gt;moves the capacity you have to whichever queue is backed up&lt;/strong&gt;, which pays off when work arrives in bursts, as uploads and exports do.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scale on the work waiting, not on CPU
&lt;/h3&gt;

&lt;p&gt;CPU is the wrong signal for a queue consumer. A worker waiting on the database or the object store uses little CPU while messages pile up behind it.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk9f8urqa0vj0iszlq9h8.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk9f8urqa0vj0iszlq9h8.png" alt=" " width="800" height="253"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Figure 11.&lt;/strong&gt; Queue depth and CPU for the same worker during an upload burst. Only one of them says "more workers needed".&lt;/p&gt;

&lt;p&gt;&lt;a href="https://keda.sh" rel="noopener noreferrer"&gt;KEDA&lt;/a&gt; scales each worker Deployment on its queue length (and for one worker on CPU too, whichever asks for more). It manages the Horizontal Pod Autoscaler itself and it reads queue sizes with a broker user that can see queues but not use them:&lt;/p&gt;

&lt;p&gt;KEDA ScaledObject&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: file-cleaner
spec:
  scaleTargetRef:
    name: file-cleaner
  minReplicaCount: 3
  maxReplicaCount: 8
  advanced:
    restoreToOriginalReplicaCount: true
    horizontalPodAutoscalerConfig:
      behavior:
        scaleDown: { stabilizationWindowSeconds: 300 }   # 5 quiet minutes before removing pods
  triggers:
    - type: rabbitmq
      metadata:
        protocol: http
        host: http://rabbitmq:15672/%2F
        queueName: file-cleaning
        mode: QueueLength
        value: "5"                                         # one pod per 5 messages waiting
      authenticationRef: { name: keda-rabbitmq-readonly }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The workflow engine is different. Its trigger queues empty almost at once into orchestrator runs, so queue length says nothing about its load. It scales on &lt;em&gt;runs waiting or running&lt;/em&gt;, through a Postgres role that can do exactly one thing, call a function returning that count:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;triggers:
  - type: postgresql
    metadata:
      userName: keda                     # EXECUTE on one function, nothing else
      dbName: orchestrator
      query: "SELECT keda_active_flow_runs()"
      targetQueryValue: "6"              # one pod per maxConcurrentRuns
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One detail for reading the numbers: the autoscaler ignores differences under 10%. With 2 pods of 6 runs each, 13 runs (6.5 per pod) doesn't add a third pod; 14 does.&lt;/p&gt;

&lt;h3&gt;
  
  
  Admission control for heavy jobs
&lt;/h3&gt;

&lt;p&gt;Scaling the workflow engine up means more Spark Jobs and those need real CPU and memory. Without a limit, more workflow pods would add load, not capacity. So the Spark Jobs go through a &lt;a href="https://kueue.sigs.k8s.io" rel="noopener noreferrer"&gt;Kueue&lt;/a&gt; queue that holds a fixed budget of CPU and memory. A Job is created suspended and starts when the running Jobs leave room for it. Its deadline starts only then, so waiting no longer counts as failing:&lt;/p&gt;

&lt;p&gt;Kueue ClusterQueue&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apiVersion: kueue.x-k8s.io/v1beta1
kind: ClusterQueue
metadata:
  name: spark
spec:
  queueingStrategy: BestEffortFIFO    # in order, but a small job may pass a large one that doesn't fit yet
  resourceGroups:
    - coveredResources: ["cpu", "memory"]
      flavors:
        - name: spark
          resources:
            - name: cpu
              nominalQuota: "8"       # the room kept for Spark beside everything else
            - name: memory
              nominalQuota: 24Gi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kueue's admission webhook refuses to create any Job in the namespace while Kueue is down. That covers setup Jobs and backup runs as well as Spark Jobs. A new dependency is also a new failure mode, so Kueue runs two replicas with a disruption budget, only manages namespaces labelled for it and has its own alert after two minutes down.&lt;/p&gt;

&lt;h3&gt;
  
  
  Requests, limits and the throttling trap
&lt;/h3&gt;

&lt;p&gt;Every container has CPU and memory requests (what the scheduler reserves) and limits (the ceiling). The two kinds of limit behave very differently:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;CPU limit&lt;/th&gt;
&lt;th&gt;Memory limit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;When exceeded&lt;/td&gt;
&lt;td&gt;The pod is &lt;strong&gt;throttled&lt;/strong&gt;: it waits for the next 100 ms period&lt;/td&gt;
&lt;td&gt;The container is &lt;strong&gt;killed&lt;/strong&gt; (OOMKilled)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What users see&lt;/td&gt;
&lt;td&gt;Slowness&lt;/td&gt;
&lt;td&gt;Restarts, redelivered work (Figure 3)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How we set it&lt;/td&gt;
&lt;td&gt;Enough for the process's real parallelism&lt;/td&gt;
&lt;td&gt;The largest input plus headroom; alert at 90%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The parallelism point caught us. Celery's prefork pool runs four child processes and the workers had a CPU limit of half a core. Four processes shared 500m, so every task ran at an eighth of a core. The Grafana panel "held back by CPU limit" showed it plainly. A 2-core limit matched the pool and the requests stayed small so the scheduler still packs pods tightly.&lt;/p&gt;

&lt;p&gt;Throttling alerts have their own trap. Throttling is measured as &lt;em&gt;throttled periods ÷ periods&lt;/em&gt;. A pod that is idle almost all the time but spikes briefly, a health check that imports a large app, say, can show 60% throttling while using 2% of its limit. Alerting on that ratio alone pages people about pods that are fine. Our alert fires only when the pod is also using a real share of its limit:&lt;/p&gt;

&lt;p&gt;PromQL (alert rule)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(
  sum by (namespace, pod) (rate(container_cpu_cfs_throttled_periods_total{namespace="platform"}[5m]))
  / sum by (namespace, pod) (rate(container_cpu_cfs_periods_total{namespace="platform"}[5m]))
) &amp;gt; 0.25
and on (namespace, pod)
(
  sum by (namespace, pod) (rate(container_cpu_usage_seconds_total{namespace="platform",container!=""}[5m]))
  / sum by (namespace, pod) (max by (namespace, pod, container) (kube_pod_container_resource_limits{namespace="platform",resource="cpu"}))
) &amp;gt; 0.5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Monitoring and observability without the internet
&lt;/h2&gt;

&lt;p&gt;Everything above assumes someone &lt;em&gt;notices&lt;/em&gt; when something goes wrong. In a bank there's no internet, often no Slack and the people on call may have no cluster access. Our monitoring had to work entirely inside the cluster and reach people only through the bank's mail relay.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1g5dfz585hz4gcp7rd28.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1g5dfz585hz4gcp7rd28.png" alt=" " width="800" height="316"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Figure 12.&lt;/strong&gt; Three signals, one place for alerts. Logs are collected from every pod, instrumented or not; traces and metrics pass through one redacting gateway; every alert is evaluated in Prometheus and leaves by email.&lt;/p&gt;
&lt;h3&gt;
  
  
  Collect from everything, redact in one place
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Logs come from the container log files&lt;/strong&gt;, not from the apps. The database, the broker and third-party components never send telemetry and they're often the ones that fail. A Vector agent on every node tags each line with its namespace, app, pod and parsed log level.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traces and metrics go through one gateway&lt;/strong&gt; (the OpenTelemetry Collector). It strips what a bank must not store, such as SQL text, credentials, cookies and URL query strings, once for every service. It also buffers through a log-store restart.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Each service is named by its &lt;code&gt;app&lt;/code&gt; label&lt;/strong&gt; everywhere, so its logs, traces, metrics and alerts join on one name.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The log store keeps its data in the platform's own object store and Postgres.&lt;/strong&gt; There's no extra disk to provision or back up.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Alerts that mean something
&lt;/h3&gt;

&lt;p&gt;Prometheus evaluates the rules and Alertmanager sends email through the bank's relay. A bank's cluster can't reach Slack or PagerDuty, but it always has a mail relay. Each alert links to a runbook: what it means, what's affected, how to check, how to fix, when to escalate. A few of the rules show the thinking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A queue with messages and nobody consuming it.&lt;/strong&gt; This catches a dead consumer within minutes, whatever the reason:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- alert: QueueNoConsumers
  expr: |
    max by (namespace, vhost, queue) (rabbitmq_detailed_queue_messages_ready{queue!~".*(retry|dead-letter).*"}) &amp;gt; 0
    and on (namespace, vhost, queue)
    max by (namespace, vhost, queue) (rabbitmq_detailed_queue_consumers) == 0
  for: 10m
  labels: { severity: critical }
  annotations:
    summary: "Queue {{ $labels.queue }} has messages but no consumer for 10m"
    runbook_url: "https://runbooks.example/#queuenoconsumers"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;A backup that silently stopped.&lt;/strong&gt; This one alerts on the absence of success, not on a failure. A CronJob that never runs (suspended, a refused admission, a broken schedule) never fails either:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- alert: BackupMissing
  expr: |
    time() - (
      max by (namespace, cronjob) (kube_cronjob_status_last_successful_time{cronjob=~".*-backup-.*"})
      or max by (namespace, cronjob) (kube_cronjob_created{cronjob=~".*-backup-.*"})   # a new CronJob counts from its creation
    ) &amp;gt; 26 * 3600
  for: 30m
  labels: { severity: critical }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Duplicates held back.&lt;/strong&gt; kube-prometheus-stack ships generic rules for the same failures. While our more specific alert fires for an object, the generic one for it is inhibited, so on-call gets one email per problem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;inhibit_rules:
  - source_matchers: [severity="critical"]
    target_matchers: [severity="warning"]
    equal: [alertname, namespace]
  - source_matchers: [alertname="PlatformPodCrashLooping"]
    target_matchers: [alertname="KubePodCrashLooping"]
    equal: [namespace, pod, container]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Other choices that keep the noise down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Error-line alerts come from a metric, not a log query.&lt;/strong&gt; Vector counts ERROR lines per app and Prometheus scrapes the count, so log alerts share the routing, grouping and silencing of every other alert.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retry and dead-letter queues are left out of backlog alerts.&lt;/strong&gt; They hold messages by design. Dead letters get their own alert.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A Job alerts only after it has used up its retries.&lt;/strong&gt; A setup Job that succeeds on its second try stays quiet.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Check from the users' side
&lt;/h3&gt;

&lt;p&gt;Every pod can be healthy while nobody can log in: a DNS change, a load balancer, an expired certificate. A blackbox exporter fetches the portal, the object store and the other endpoints, both inside the cluster and at their public addresses. It records whether each answered, how fast and when its certificate expires. Vector also turns the portal's access log into request counts and response times, which works behind any ingress controller or OpenShift's router.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mark every change on every chart
&lt;/h3&gt;

&lt;p&gt;The first question in an incident is "what changed?". Grafana marks every Helm install, upgrade and rollback and every database failover, on every chart. They come from metrics Prometheus already has (kube-state-metrics exposes the Helm release Secrets and CloudNativePG's exporter exposes the primary), so nothing has to be recorded by hand.&lt;/p&gt;

&lt;h3&gt;
  
  
  A daily report, which is also a dead man's switch
&lt;/h3&gt;

&lt;p&gt;Nothing reports to us from a bank on its own. So a CronJob builds a support bundle inside the cluster every morning, with read-only access. That covers pod states, events, recent logs, memory and CPU pressure, queue depths, database activity and Helm values with every credential redacted. It keeps the last 14 bundles in a bucket and emails a short report: each component's state, the problems found and what to do about each. It contains no logs and no data.&lt;/p&gt;

&lt;p&gt;The report has a second use. &lt;strong&gt;When an install's report doesn't arrive by mid-morning, we ask.&lt;/strong&gt; Its monitoring, its relay or the platform itself may be down and a platform that is down can't send alerts. A signal that must keep arriving is the only way to detect the failure of the alerting path itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Case study: the disk alert that wasn't about volumes.&lt;/strong&gt; Volume-filling alerts fired on a single-server install, yet the volumes held little. On k3s, local-path volumes have no quota, so they report the node's whole disk and the disk was 82% full of old container images from months of upgrades. Pruning brought it to 58%. The lasting fix wasn't a cron job, which banks dislike on their servers. It was the kubelet's own image garbage collection, set to start at 75% disk use and stop at 65%.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# /etc/rancher/k3s/config.yaml (OpenShift: a KubeletConfig with imageGCHighThresholdPercent)
kubelet-arg:
  - "image-gc-high-threshold=75"
  - "image-gc-low-threshold=65"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  High availability is not a backup
&lt;/h2&gt;

&lt;p&gt;Replication protects against a machine dying. It does nothing against a bad upgrade, a deleted table or a corrupted volume: those are copied faithfully to every replica within milliseconds. That needs copies stored somewhere else and a restore that is scripted.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F73tj1wrms9srrljra0bu.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F73tj1wrms9srrljra0bu.png" alt=" " width="800" height="253"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Figure 13.&lt;/strong&gt; Replication and backups solve different problems. Point-in-time recovery always builds a new cluster, so the archive it reads from is never overwritten and a recovery that goes wrong can be repeated.&lt;/p&gt;

&lt;p&gt;Three copies leave the cluster, all to the bank's own S3-compatible storage. That might be a storage appliance or a separate MinIO, ideally in another data centre. It is never the in-cluster object store, which is one of the things being backed up.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;th&gt;How&lt;/th&gt;
&lt;th&gt;Kept&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Postgres WAL and base backups&lt;/td&gt;
&lt;td&gt;CloudNativePG's Barman Cloud plugin: each WAL segment when full or after 5 minutes; a base backup daily&lt;/td&gt;
&lt;td&gt;35 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Every database and role&lt;/td&gt;
&lt;td&gt;A nightly &lt;code&gt;pg_dumpall&lt;/code&gt; CronJob: a plain, portable copy for the scripted restore&lt;/td&gt;
&lt;td&gt;35 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Every bucket&lt;/td&gt;
&lt;td&gt;A nightly or hourly &lt;code&gt;rclone copy&lt;/code&gt;, which never deletes on the target&lt;/td&gt;
&lt;td&gt;Never deleted&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apiVersion: batch/v1
kind: CronJob
metadata:
  name: backup-postgres
spec:
  schedule: "0 1 * * *"
  concurrencyPolicy: Forbid          # a slow night never overlaps the next one
  jobTemplate:
    spec:
      backoffLimit: 2
      template:
        spec:
          containers:
            - name: upload
              command: ["/bin/sh", "-c"]
              args:
                - |
                  set -e
                  rclone copy /dump target:$BUCKET/postgres
                  rclone delete --min-age 35d target:$BUCKET/postgres
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Two stores must agree
&lt;/h3&gt;

&lt;p&gt;An Iceberg table has two halves: data files in the object store and a catalog in Postgres that says which files make up each table. Postgres's WAL can be hours newer than the nightly bucket copy. Restored to its latest point, the catalog would point at data files the restored store doesn't have.&lt;/p&gt;

&lt;p&gt;So the bucket copy writes a &lt;code&gt;LAST_BACKUP&lt;/code&gt; marker with its start time to the backup target. Recovery puts Postgres back to that moment, not later. Keeping the marker off-cluster means the consistent point is still known after the whole cluster is gone. For a smaller gap, copy the buckets more often than you dump the database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A restore you haven't run is a hope, not a plan.&lt;/strong&gt; The restore is a script that scales everything down, refuses to continue while anything else is connected to the database, restores and scales back up. The go-live checklist includes a timed restore drill, so the recovery time the bank signs off on is a measured number.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Every hop between services goes through a durable, replicated queue; messages are acknowledged after the work&lt;/li&gt;
&lt;li&gt;Memory limits are sized for the largest input and OOM kills and restarts alert&lt;/li&gt;
&lt;li&gt;Queue consumers have a heartbeat written by the working loop and a liveness probe that checks it&lt;/li&gt;
&lt;li&gt;Locks are single conditional writes with an owner, released on every failure path&lt;/li&gt;
&lt;li&gt;Migrations take a lock; the scheduler runs exactly once (&lt;code&gt;Recreate&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Every process handles &lt;code&gt;SIGTERM&lt;/code&gt;: stop taking work, finish, acknowledge, exit; grace periods fit the longest job&lt;/li&gt;
&lt;li&gt;The database replicates synchronously to at least one replica, on another server&lt;/li&gt;
&lt;li&gt;Queues are quorum queues; broker nodes wait for quorum before stopping&lt;/li&gt;
&lt;li&gt;Replicas spread across servers and have disruption budgets, without breaking single-server installs&lt;/li&gt;
&lt;li&gt;Heavy jobs run in their own pods, admitted when there is room&lt;/li&gt;
&lt;li&gt;Workers scale on work waiting, not CPU; CPU limits match the process's real parallelism&lt;/li&gt;
&lt;li&gt;Alerts arrive by a channel the site allows, each with a runbook; duplicates are inhibited&lt;/li&gt;
&lt;li&gt;Something must keep arriving every day, so a silent install is noticed&lt;/li&gt;
&lt;li&gt;Backups leave the cluster, the database and object store restore to the same moment and the restore has been timed&lt;/li&gt;
&lt;li&gt;The "what survives what" table is current, including what isn't covered&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these ideas are new. What changes on a bank's servers is that no provider applies them for you, so each one becomes a line in a chart, a probe or a runbook. Writing them down, with the reason next to each, is what lets the next engineer change one without breaking the others.&lt;/p&gt;

</description>
      <category>sre</category>
      <category>platformengineering</category>
    </item>
    <item>
      <title>Apache Iceberg in a data lake: how it works and how to keep it fast, cheap and simple</title>
      <dc:creator>Josephat Macharia</dc:creator>
      <pubDate>Fri, 30 Oct 2026 03:20:48 +0000</pubDate>
      <link>https://dev.to/mashaa/apache-iceberg-in-a-data-lake-how-it-works-and-how-to-keep-it-fast-cheap-and-simple-509m</link>
      <guid>https://dev.to/mashaa/apache-iceberg-in-a-data-lake-how-it-works-and-how-to-keep-it-fast-cheap-and-simple-509m</guid>
      <description>&lt;p&gt;An S3 bucket full of Parquet files is not a table. Iceberg turns it into one. Here is what happens under the hood, the traps we fell into and the settings that made our queries faster and our bill smaller.&lt;/p&gt;

&lt;p&gt;Field notes from running Iceberg tables for transaction reconciliation on S3, Glue, Spark and Athena&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem Iceberg solves
&lt;/h2&gt;

&lt;p&gt;A classic data lake is just folders of files. To answer a query, the engine lists the folders, opens the files and hopes nobody was halfway through writing one. There are no atomic commits, no safe way to update a row and the schema is whatever the last writer decided.&lt;/p&gt;

&lt;p&gt;Iceberg adds a thin layer of &lt;strong&gt;metadata files&lt;/strong&gt; that record exactly which data files make up the table at each moment. The data stays as plain Parquet on S3. Everything clever happens in the metadata.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Iceberg works: a tree of metadata
&lt;/h2&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy39yz9l9hm3s5gwx5r75.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy39yz9l9hm3s5gwx5r75.png" alt=" " width="799" height="518"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Figure 1.&lt;/strong&gt; Every Iceberg table is a tree. The catalog holds one pointer. Everything below it is an immutable file on S3, so a reader always sees one consistent snapshot.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A commit&lt;/strong&gt; writes new data files, then new metadata, then swaps the catalog pointer in one atomic step. Readers see all of the change or none of it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A snapshot&lt;/strong&gt; is an immutable list of files. Old snapshots stay until you expire them, which is what gives you time travel and rollback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Engines never list S3 folders.&lt;/strong&gt; They read the manifests, which already know every file's partition and per-column min/max values.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  How a read finds its data
&lt;/h2&gt;

&lt;p&gt;That last point is where the speed comes from. Before opening a single Parquet file, the engine uses metadata to throw away files that can't contain a match. It skips data in three layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Partition pruning.&lt;/strong&gt; Manifests record each file's partition values. With a table partitioned by &lt;code&gt;account_id, months(transaction_date)&lt;/code&gt;, a filter on those columns drops whole accounts and months before any file is opened.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File-level min/max statistics.&lt;/strong&gt; Manifests also store each column's min and max for every file. A filter like &lt;code&gt;source_ref = 'TX-88213'&lt;/code&gt; skips every file whose range can't contain that value.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parquet row-group statistics.&lt;/strong&gt; Inside each file that survives, rows are stored in chunks called row groups, each with its own min/max per column. Whole chunks are skipped the same way.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgeyftqkwocr6md3ocnyh.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgeyftqkwocr6md3ocnyh.png" alt=" " width="800" height="344"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Figure 2.&lt;/strong&gt; Three layers of skipping. The first two use only Iceberg metadata, so they cost almost nothing. A query that skips all three layers reads the whole table.&lt;/p&gt;
&lt;h3&gt;
  
  
  Partition pruning, with a real query
&lt;/h3&gt;

&lt;p&gt;Our table is partitioned by &lt;code&gt;(client_id, account_id, months(transaction_date))&lt;/code&gt;. Every file belongs to exactly one partition, a tuple such as &lt;code&gt;(42, 7, 2026-10)&lt;/code&gt; and Iceberg writes that tuple into the file's manifest entry. Take this 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="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transactions&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;client_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;account_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;transaction_date&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="s1"&gt;'2026-10-01'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;transaction_date&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;  &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="s1"&gt;'2026-10-13'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpem0on4b5wzr6mnggafh.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpem0on4b5wzr6mnggafh.png" alt=" " width="799" height="353"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Figure 3.&lt;/strong&gt; Partition pruning works in two passes over metadata. The manifest list rules out whole manifests and each remaining manifest rules out individual files. No data file is touched.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Translate the filter.&lt;/strong&gt; The query never mentions a month. Because the partition spec says &lt;code&gt;months(transaction_date)&lt;/code&gt;, Iceberg derives &lt;code&gt;month = 2026-10&lt;/code&gt; from the date range on its own. This is hidden partitioning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read the manifest list.&lt;/strong&gt; It stores, for each manifest, the lowest and highest value of every partition field it covers. &lt;code&gt;m1&lt;/code&gt; only covers July to September and &lt;code&gt;m3&lt;/code&gt; only covers clients 55 to 91, so neither is opened.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read the surviving manifest.&lt;/strong&gt; Each entry in &lt;code&gt;m2&lt;/code&gt; names one data file and its exact partition tuple. Only the five files in &lt;code&gt;(42, 7, 2026-10)&lt;/code&gt; match.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hand the five files to the next layer.&lt;/strong&gt; All of this came from a handful of small metadata files.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;When it doesn't help:&lt;/strong&gt; the filter isn't on a partition column (&lt;code&gt;reporting_tag&lt;/code&gt;, &lt;code&gt;id&lt;/code&gt;, a map key), or the partition column is hidden inside a function the planner can't see through. The &lt;code&gt;MERGE&lt;/code&gt; case study (Figure 14) is that second trap at full price.&lt;/p&gt;
&lt;h3&gt;
  
  
  File-level min/max statistics, with a real manifest
&lt;/h3&gt;

&lt;p&gt;Partition pruning stops at the partition. The five files left in &lt;code&gt;(42, 7, 2026-10)&lt;/code&gt; all have the same partition tuple, so it can't tell them apart. The next layer can. Each manifest entry also stores, for every column, the file's &lt;strong&gt;min value, max value, null count and row count&lt;/strong&gt;. The query now adds one more filter:&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;AND&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Finc24k6sjcnudhlm0oql.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Finc24k6sjcnudhlm0oql.png" alt=" " width="800" height="338"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Figure 4.&lt;/strong&gt; Each bar is one file's min → max range, read from its manifest entry. A file is opened only if &lt;em&gt;every&lt;/em&gt; filter could match inside its ranges. Here only f4 passes both.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Check the date.&lt;/strong&gt; f1 ends on Oct 6, f2 ends on Oct 11 and f5 starts on Oct 16, so none can hold Oct 12. f3 and f4 both span Oct 12 because they came from different write batches and nothing stops two files' ranges from overlapping.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the amount.&lt;/strong&gt; f3's largest amount is 87,000, so &lt;code&gt;amount &amp;gt; 100000&lt;/code&gt; can't match. f4's maximum is 250,000, so it might.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open f4 only.&lt;/strong&gt; Inside it, the row-group statistics in the next section narrow things further.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;When it doesn't help:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Values are spread across every file.&lt;/strong&gt; Unsorted data gives every file a range of Oct 1 → Oct 31, so all five are opened. That's why the sort order matters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The value is inside a map.&lt;/strong&gt; All keys share one value column, so its min/max mixes every key together (Figure 21).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The column has no statistics.&lt;/strong&gt; By default Iceberg only collects them for the first 100 columns (&lt;code&gt;write.metadata.metrics.max-inferred-column-defaults&lt;/code&gt;). For strings it keeps only the first 16 characters, which can blur long prefixes such as &lt;code&gt;TXN-2026-10-…&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The filter is a not-equals.&lt;/strong&gt; &lt;code&gt;is_reversal != true&lt;/code&gt; can't be ruled out from a min/max range, but &lt;code&gt;is_reversal = false&lt;/code&gt; can.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Row-group statistics, with a real file
&lt;/h3&gt;

&lt;p&gt;The third layer happens inside a Parquet file, so it helps to see one. Parquet doesn't store a file as one block of rows. It cuts the rows into &lt;strong&gt;row groups&lt;/strong&gt;, about 128 MB each by default (&lt;code&gt;write.parquet.row-group-size-bytes&lt;/code&gt;). Inside each row group, every column is stored separately as a &lt;strong&gt;column chunk&lt;/strong&gt;. At the end of the file sits a small &lt;strong&gt;footer&lt;/strong&gt; that records, for every row group and every column, the min value, the max value and the null count.&lt;/p&gt;

&lt;p&gt;Take a 512 MB file of October transactions, sorted by &lt;code&gt;transaction_date&lt;/code&gt; and this 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="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transactions&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;transaction_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="s1"&gt;'2026-10-12'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq84dsgmin5ihzg18rywd.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq84dsgmin5ihzg18rywd.png" alt=" " width="800" height="375"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Figure 3.&lt;/strong&gt; Left: how the file is laid out. Right: what its footer says. Only the highlighted chunks are fetched from S3: 3 of the 16 chunks.&lt;/p&gt;

&lt;p&gt;Here is what the reader does, in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Reads the footer first.&lt;/strong&gt; It's a few KB at the end of the file, fetched with one small S3 range request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Checks the filter against each row group's range.&lt;/strong&gt; Oct 12 can't be in Oct 1 → Oct 8, so row group 1 is skipped. Row group 2 covers Oct 8 → Oct 16, so it &lt;em&gt;might&lt;/em&gt; contain matches. Row groups 3 and 4 start after Oct 12, so they're skipped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fetches only the column chunks it needs from row group 2:&lt;/strong&gt; &lt;code&gt;transaction_date&lt;/code&gt; to filter, plus &lt;code&gt;id&lt;/code&gt; and &lt;code&gt;amount&lt;/code&gt; to return. The &lt;code&gt;metadata&lt;/code&gt; chunk is never downloaded. This is the columnar part of Parquet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filters the rows in memory.&lt;/strong&gt; "Might contain" isn't "does contain". The reader still checks each row in row group 2 and keeps the ones dated Oct 12.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;code&gt;amount&lt;/code&gt; column shows why the order matters. A filter like &lt;code&gt;amount &amp;gt; 300000&lt;/code&gt; skips all four row groups, because every max is below it. But &lt;code&gt;amount = 5000&lt;/code&gt; falls inside every range, so nothing can be skipped. Statistics only help on columns whose values are clustered and in our tables that means the column the data is sorted by. Shuffle the same rows randomly and every row group reports Oct 1 → Oct 31, so the Oct 12 query reads all four (Figure 18 shows that case).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Layer 2: file statistics&lt;/th&gt;
&lt;th&gt;Layer 3: row-group statistics&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Where they live&lt;/td&gt;
&lt;td&gt;Iceberg manifests&lt;/td&gt;
&lt;td&gt;The footer of each Parquet file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What gets skipped&lt;/td&gt;
&lt;td&gt;Whole files, without opening them&lt;/td&gt;
&lt;td&gt;Row groups inside a file that's already been opened&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost to check&lt;/td&gt;
&lt;td&gt;Nothing extra; read during planning&lt;/td&gt;
&lt;td&gt;One small read per surviving file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Matters most when&lt;/td&gt;
&lt;td&gt;A partition has many files&lt;/td&gt;
&lt;td&gt;A partition is one or a few large files&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; a query is fast when its &lt;code&gt;WHERE&lt;/code&gt; clause hits partition columns and columns whose values are tightly grouped inside each file. Everything in the "faster queries" section below is about making that true.&lt;/p&gt;
&lt;h2&gt;
  
  
  How writes commit and why they collide
&lt;/h2&gt;

&lt;p&gt;Iceberg uses optimistic concurrency. Each writer assumes it is alone, does its work, then tries to swap the pointer. If someone else committed first, it has to re-check and retry.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2xjx72tfialvpid03fte.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2xjx72tfialvpid03fte.png" alt=" " width="800" height="344"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Figure 3.&lt;/strong&gt; Conflicts are counted per table. Two jobs writing to different tables never wait on each other. Ten jobs writing to one table take turns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch out:&lt;/strong&gt; if you see many &lt;code&gt;CommitFailedException&lt;/code&gt; retries, you have too many writers on one table. Batch small writes into fewer commits, keep independent write paths in separate tables and raise &lt;code&gt;commit.retry.num-retries&lt;/code&gt; only as a safety net.&lt;/p&gt;
&lt;h2&gt;
  
  
  Updates and deletes: two strategies
&lt;/h2&gt;

&lt;p&gt;Parquet files can't be edited. So when you &lt;code&gt;UPDATE&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt; or &lt;code&gt;MERGE&lt;/code&gt;, Iceberg either rewrites files or writes small "delete files" next to them.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9tyu7cumky3c3v138ids.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9tyu7cumky3c3v138ids.png" alt=" " width="800" height="373"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Figure 4.&lt;/strong&gt; Copy-on-write makes writes pay. Merge-on-read makes reads pay and the bill grows with every unmerged delete file until compaction folds them back in.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Workload&lt;/th&gt;
&lt;th&gt;Choose&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Frequent small &lt;code&gt;MERGE&lt;/code&gt;s or updates (reconciliation, CDC)&lt;/td&gt;
&lt;td&gt;Merge-on-read&lt;/td&gt;
&lt;td&gt;Rewriting 512 MB to flip one flag is wasteful. Compact on a schedule.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rare, large batch updates; read-heavy dashboards&lt;/td&gt;
&lt;td&gt;Copy-on-write&lt;/td&gt;
&lt;td&gt;Readers never pay a merge cost.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Append-only event logs&lt;/td&gt;
&lt;td&gt;Either&lt;/td&gt;
&lt;td&gt;No deletes, so the choice doesn't matter.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Set it per operation with &lt;code&gt;write.update.mode&lt;/code&gt;, &lt;code&gt;write.delete.mode&lt;/code&gt; and &lt;code&gt;write.merge.mode&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Why does an update create a delete file?
&lt;/h3&gt;

&lt;p&gt;Because Parquet files can't be edited, Iceberg has no real "update". Every &lt;code&gt;UPDATE&lt;/code&gt; is carried out as two steps: &lt;strong&gt;delete the old version, then insert the new one&lt;/strong&gt;. The write mode decides how the delete half is done.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Delete the old version&lt;/th&gt;
&lt;th&gt;Insert the new version&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Copy-on-write&lt;/td&gt;
&lt;td&gt;Rewrite file A without the old row&lt;/td&gt;
&lt;td&gt;The new row goes into that rewritten file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Merge-on-read&lt;/td&gt;
&lt;td&gt;Write a &lt;strong&gt;delete file&lt;/strong&gt;: "file A, row 51207 is gone"&lt;/td&gt;
&lt;td&gt;Write a &lt;strong&gt;new data file&lt;/strong&gt; holding the new row&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So a delete file is merge-on-read's cheap way of saying "this old version no longer counts" without rewriting a 1M-row file. A plain &lt;code&gt;DELETE&lt;/code&gt; works the same way minus the insert: it produces only a delete file. A &lt;code&gt;MERGE&lt;/code&gt; is the same again: matched rows are deleted and re-inserted.&lt;/p&gt;
&lt;h3&gt;
  
  
  The three kinds of delete file
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Kind&lt;/th&gt;
&lt;th&gt;What it says&lt;/th&gt;
&lt;th&gt;Who writes it&lt;/th&gt;
&lt;th&gt;Cost at read time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Position delete&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;"file A, row 51207"&lt;/td&gt;
&lt;td&gt;Spark, Athena and Trino &lt;code&gt;UPDATE&lt;/code&gt; / &lt;code&gt;DELETE&lt;/code&gt; / &lt;code&gt;MERGE&lt;/code&gt;. The engine already scanned for the rows, so it knows exactly where they are.&lt;/td&gt;
&lt;td&gt;Low: skip known positions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Equality delete&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;"any row where &lt;code&gt;id = 42&lt;/code&gt;"&lt;/td&gt;
&lt;td&gt;Streaming upserts (for example Flink), which don't look the row up first&lt;/td&gt;
&lt;td&gt;High: every row of every older data file in scope is compared&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Deletion vector&lt;/strong&gt; (format v3)&lt;/td&gt;
&lt;td&gt;A compact bitmap of deleted row positions, one per data file&lt;/td&gt;
&lt;td&gt;The same engines, on v3 tables&lt;/td&gt;
&lt;td&gt;Lowest: one small file per data file, replaced on each commit instead of piling up&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h3&gt;
  
  
  Why the new row isn't deleted too
&lt;/h3&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frhj8xpcu7xb1mibode3j.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frhj8xpcu7xb1mibode3j.png" alt=" " width="800" height="295"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Figure 5.&lt;/strong&gt; Every commit gets an increasing sequence number. A position delete names the exact file it applies to. An equality delete only applies to data files with a lower sequence number. Either way, the new row written in the same commit is safe.&lt;/p&gt;
&lt;h3&gt;
  
  
  The flow of a &lt;code&gt;DELETE&lt;/code&gt;
&lt;/h3&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9hc6z1cblqsn8ejfrsuv.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9hc6z1cblqsn8ejfrsuv.png" alt=" " width="800" height="577"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Figure 6.&lt;/strong&gt; A &lt;code&gt;DELETE&lt;/code&gt; from plan to physical removal. Figure 7 shows how an &lt;code&gt;UPDATE&lt;/code&gt; differs.&lt;/p&gt;
&lt;h3&gt;
  
  
  The flow of an &lt;code&gt;UPDATE&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;An update follows the same path as a delete, with two differences. It always writes the new row versions, so there is no metadata-only shortcut. And at step 3 the two write modes split: copy-on-write rewrites files, while merge-on-read writes a delete file plus a new data file. A &lt;code&gt;MERGE&lt;/code&gt; works the same way: matched rows take this path and unmatched rows are plain inserts.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0ji864okh8q8lr70ubj8.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0ji864okh8q8lr70ubj8.png" alt=" " width="800" height="557"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Figure 7.&lt;/strong&gt; An &lt;code&gt;UPDATE&lt;/code&gt; from plan to physical removal. The two write modes only differ at step 3; everything after that is shared, though compaction matters far more for merge-on-read.&lt;/p&gt;
&lt;h3&gt;
  
  
  Merge-on-read, step by step: what happens to the delete file?
&lt;/h3&gt;

&lt;p&gt;A common misreading is that the one-row file gets merged into file A when you read it and the delete file then disappears. It doesn't. Reads never change anything on disk. The files are only combined when compaction runs and only removed from S3 when the old snapshot expires.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fenus2at9z6cb79cczgqw.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fenus2at9z6cb79cczgqw.png" alt=" " width="799" height="353"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Figure 8.&lt;/strong&gt; The lifecycle of one merge-on-read update. The delete file stops being used at step 3 and stops costing money at step 4.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The one-row file is never appended to file A.&lt;/strong&gt; Parquet files are immutable. Compaction writes a brand-new file A′ containing A's surviving rows plus the updated row and it may pack other small files from the same partition in with them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The delete file is dropped from the table, then from S3.&lt;/strong&gt; Once every data file it points at has been rewritten, the new snapshot no longer lists it. It is physically deleted only when &lt;code&gt;expire_snapshots&lt;/code&gt; removes the last snapshot that uses it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Until compaction runs, every read pays.&lt;/strong&gt; Ten &lt;code&gt;MERGE&lt;/code&gt;s on one partition mean ten delete files to apply on every query. That's why merge-on-read tables need compaction on a schedule.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Newer format, same idea.&lt;/strong&gt; Iceberg format v3 replaces per-commit position delete files with one compact "deletion vector" per data file. That makes reads cheaper, but you still need compaction and snapshot expiry.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Watch out:&lt;/strong&gt; a deleted row stays on S3 until both compaction and snapshot expiry have run. If you handle data-removal requests (GDPR-style "erase my data"), your deadline includes those two jobs, not just the &lt;code&gt;DELETE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; a delete whose filter lines up with partitions is almost free. On a table partitioned by &lt;code&gt;account_id, months(transaction_date)&lt;/code&gt;, &lt;code&gt;DELETE ... WHERE account_id = 42 AND transaction_date &amp;lt; TIMESTAMP '2026-01-01'&lt;/code&gt; can drop whole files without writing anything.&lt;/p&gt;
&lt;h3&gt;
  
  
  When &lt;code&gt;UPDATE&lt;/code&gt; and &lt;code&gt;DELETE&lt;/code&gt; get slow
&lt;/h3&gt;

&lt;p&gt;Almost every slow update or delete comes down to one of two things: step 2 has to scan too much, or step 3 has to write too much.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Why it's slow&lt;/th&gt;
&lt;th&gt;What to do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The &lt;code&gt;WHERE&lt;/code&gt; clause has no partition filter&lt;/td&gt;
&lt;td&gt;Step 2 scans every file in the table to find the rows&lt;/td&gt;
&lt;td&gt;Always include the partition columns, for example account and date range&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Filtering on a map key, JSON field or a function of a column&lt;/td&gt;
&lt;td&gt;No file can be skipped, so the scan is as wide as the partition filter allows&lt;/td&gt;
&lt;td&gt;Promote the field to a real column; compare raw columns to literals&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scattered changes on a copy-on-write table&lt;/td&gt;
&lt;td&gt;One changed row per file still rewrites the whole file. 1,000 rows spread over 1,000 files means 1,000 × 512 MB rewritten&lt;/td&gt;
&lt;td&gt;Use merge-on-read for scattered changes, or sort the table so rows that change together live in the same files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Many small statements, such as an update per row in a loop&lt;/td&gt;
&lt;td&gt;Each statement scans, writes, commits and adds a snapshot and its own delete files&lt;/td&gt;
&lt;td&gt;Collect the changes and apply them in one &lt;code&gt;MERGE&lt;/code&gt; (below)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Several jobs changing the same partitions at once&lt;/td&gt;
&lt;td&gt;Conflicts are detected at commit time, after all the work is done, so the loser redoes the whole statement&lt;/td&gt;
&lt;td&gt;One writer per table or partition at a time; batch changes; keep filters narrow so conflict checks are scoped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delete files have piled up&lt;/td&gt;
&lt;td&gt;Step 2 has to apply all existing delete files while it scans, before it can even find the rows&lt;/td&gt;
&lt;td&gt;Compact merge-on-read tables on a schedule&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rewriting most of a partition with merge-on-read&lt;/td&gt;
&lt;td&gt;You get a delete file listing almost every row, plus a full copy of the new rows. Reads pay for both until compaction&lt;/td&gt;
&lt;td&gt;For bulk changes, use copy-on-write for that operation, or &lt;code&gt;INSERT OVERWRITE&lt;/code&gt; the affected partitions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Equality deletes from streaming upserts&lt;/td&gt;
&lt;td&gt;Every read compares rows against them&lt;/td&gt;
&lt;td&gt;Compact often; prefer writers that produce position deletes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h3&gt;
  
  
  How &lt;code&gt;MERGE&lt;/code&gt; works
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;MERGE&lt;/code&gt; takes a &lt;strong&gt;source&lt;/strong&gt; of changes (a staging table or query) and applies it to a &lt;strong&gt;target&lt;/strong&gt; table in one statement. It joins the two, decides per row whether to update, delete or insert, writes the results once and commits once.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbymfhakwy1uwmu5pf417.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbymfhakwy1uwmu5pf417.png" alt=" " width="800" height="365"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Figure 9.&lt;/strong&gt; A &lt;code&gt;MERGE&lt;/code&gt; is an update, a delete and an insert in one statement. However many rows it changes, it scans the target once and produces one snapshot.&lt;/p&gt;
&lt;h3&gt;
  
  
  Is &lt;code&gt;MERGE&lt;/code&gt; the better way to change lots of rows?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;For a batch of row-specific changes, yes.&lt;/strong&gt; If you have 10,000 transactions that each need a different new value, a &lt;code&gt;MERGE&lt;/code&gt; from a staging table does all of them in one pass. The same changes as 10,000 &lt;code&gt;UPDATE&lt;/code&gt; statements repeat the whole flow 10,000 times.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fceu98zo2bhmmakinju0w.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fceu98zo2bhmmakinju0w.png" alt=" " width="800" height="305"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Figure 10.&lt;/strong&gt; The win comes from batching, not from the keyword. Each statement carries a fixed cost: scan, commit, snapshot and conflict check. &lt;code&gt;MERGE&lt;/code&gt; pays it once.&lt;/p&gt;

&lt;p&gt;It isn't automatically better in every case:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One set-based change:&lt;/strong&gt; &lt;code&gt;UPDATE t SET status = 'closed' WHERE batch_id = 7&lt;/code&gt; is already a single pass. A &lt;code&gt;MERGE&lt;/code&gt; would do the same work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Most of a partition changes:&lt;/strong&gt; &lt;code&gt;INSERT OVERWRITE&lt;/code&gt; of that partition is cheaper. It writes new files and drops the old ones, with no delete files at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The target isn't pruned:&lt;/strong&gt; a &lt;code&gt;MERGE&lt;/code&gt; without partition filters on the target scans the whole table, which can be slower than a well-filtered &lt;code&gt;UPDATE&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Writing a fast &lt;code&gt;MERGE&lt;/code&gt;
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;MERGE&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transactions&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;staging&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;batch_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;      &lt;span class="c1"&gt;-- deduplicated: one row per id&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt;  &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;account_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;                               &lt;span class="c1"&gt;-- prune the target&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transaction_date&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="s1"&gt;'2026-09-01'&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transaction_date&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;  &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="s1"&gt;'2026-10-01'&lt;/span&gt;
&lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;MATCHED&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'delete'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt;
&lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;MATCHED&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;  &lt;span class="c1"&gt;-- skip rows that didn't change&lt;/span&gt;
  &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated&lt;/span&gt;
&lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;MATCHED&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                     &lt;span class="c1"&gt;-- Spark syntax; Athena needs a column list&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Put the target's partition filters in the &lt;code&gt;ON&lt;/code&gt; clause.&lt;/strong&gt; They prune the target scan and also narrow the conflict check, so concurrent jobs on other partitions don't clash.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deduplicate the source&lt;/strong&gt; to one row per key. Duplicates make the &lt;code&gt;MERGE&lt;/code&gt; fail.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skip no-op rows&lt;/strong&gt; with a condition such as &lt;code&gt;IS DISTINCT FROM&lt;/code&gt;. Without it, unchanged rows still get a delete and a re-insert.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the source small and filtered.&lt;/strong&gt; A small source can be broadcast to every worker instead of shuffling both sides of the join.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run one &lt;code&gt;MERGE&lt;/code&gt; per table per batch,&lt;/strong&gt; not several in parallel on the same partitions.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Case study: the &lt;code&gt;ON&lt;/code&gt; clause that scanned 130 GB
&lt;/h3&gt;

&lt;p&gt;That first bullet cost us more than any other mistake. Our reconciliation rules are &lt;code&gt;MERGE&lt;/code&gt; statements on the transactions table itself. The source query finds matched pairs and the &lt;code&gt;MERGE&lt;/code&gt; flags them. The source was carefully filtered to one client, a few accounts and a 30-day window. The &lt;code&gt;ON&lt;/code&gt; clause looked like this:&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="n"&gt;MERGE&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transactions&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;client_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;array_agg&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;AS&lt;/span&gt; &lt;span class="n"&gt;matched_ids&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transactions&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;client_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;account_id&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;-- source is pruned&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transaction_date&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;date_parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-09-01'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'%Y-%m-%d'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transaction_date&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;date_parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-09-30'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'%Y-%m-%d'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt;  &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client_id&lt;/span&gt;                         &lt;span class="c1"&gt;-- join condition, not a filter&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;matched_ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&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="c1"&gt;-- can't prune anything&lt;/span&gt;
&lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;MATCHED&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;matched&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;MERGE&lt;/code&gt; is a join between the source and the target. Filters inside the &lt;code&gt;USING&lt;/code&gt; subquery only prune the source's own scan and they don't carry over to the target. On the target side, nothing compared a partition column to a constant. &lt;code&gt;r.client_id = t.client_id&lt;/code&gt; is a join condition and &lt;code&gt;contains(array, r.id)&lt;/code&gt; is opaque to the planner. So Athena read every partition of the target to look for matches.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feufmh79nwaf8wadic4gy.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feufmh79nwaf8wadic4gy.png" alt=" " width="799" height="388"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Figure 11.&lt;/strong&gt; The same &lt;code&gt;MERGE&lt;/code&gt; before and after. The source was always pruned. The target was not, until its filters were written into the &lt;code&gt;ON&lt;/code&gt; clause.&lt;/p&gt;

&lt;p&gt;The fix was to repeat the source's static filters on the target alias, inside &lt;code&gt;ON&lt;/code&gt;:&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;ON&lt;/span&gt;  &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client_id&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;                                  &lt;span class="c1"&gt;-- static filters on the target:&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;account_id&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                               &lt;span class="c1"&gt;-- these prune the target scan&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transaction_date&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;date_parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-09-01'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'%Y-%m-%d'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transaction_date&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;date_parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-09-30'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'%Y-%m-%d'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;matched_ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;One rule, one account, 30-day window&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Data scanned&lt;/td&gt;
&lt;td&gt;129.72 GB&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;984 MB&lt;/strong&gt; (99.2% less)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime&lt;/td&gt;
&lt;td&gt;4 min 31 s&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;20 s&lt;/strong&gt; (about 13× faster)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Athena cost per run, at $5 per TB scanned&lt;/td&gt;
&lt;td&gt;about $0.63&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;about $0.005&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We then applied the same pattern to every rule file. Across the board, data scanned per query fell by about 98%, from over 100 GB to under 10 GB. Because a reconciliation run executes dozens of these rules one after another, the savings multiply.&lt;/p&gt;

&lt;p&gt;The same pass also made some smaller fixes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Removed redundant &lt;code&gt;date_trunc('month', CAST(transaction_date AS DATE))&lt;/code&gt; filters.&lt;/strong&gt; They added nothing the day-range filter didn't already say and a function wrapped around the column is the form planners are least likely to prune on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trimmed unused columns from the &lt;code&gt;UNNEST&lt;/code&gt; subqueries.&lt;/strong&gt; Parquet is columnar, so every column you don't select is bytes never read and fewer bytes to shuffle into the join.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dropped an &lt;code&gt;ORDER BY&lt;/code&gt; in the source.&lt;/strong&gt; A &lt;code&gt;MERGE&lt;/code&gt; doesn't care about the order of its source rows, so sorting them was pure cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrote &lt;code&gt;is_reversal = false&lt;/code&gt; instead of &lt;code&gt;is_reversal != true&lt;/code&gt;.&lt;/strong&gt; Both exclude &lt;code&gt;NULL&lt;/code&gt;, but the equality form is a plain value that file statistics can check.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; every filter on the source of a &lt;code&gt;MERGE&lt;/code&gt; belongs on the target too, written against the target alias in the &lt;code&gt;ON&lt;/code&gt; clause. Then check the "Data scanned" figure in the query stats. In Athena, that number is also your bill.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things to watch for
&lt;/h2&gt;

&lt;p&gt;Most Iceberg problems are slow leaks. Each write leaves a little behind: a small file, a delete file, a snapshot, a manifest. Nothing breaks on day one. By month three queries are slow and the S3 bill has doubled.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7kwnjwr014uh1jrqjujf.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7kwnjwr014uh1jrqjujf.png" alt=" " width="799" height="324"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Figure 11.&lt;/strong&gt; Small files are the most common Iceberg tax. Every file costs an S3 request, a manifest entry and some planning time, whatever its size.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Usual cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Queries slow down week by week&lt;/td&gt;
&lt;td&gt;Thousands of small files from frequent small writes&lt;/td&gt;
&lt;td&gt;Compact with &lt;code&gt;rewrite_data_files&lt;/code&gt;; batch writes; set &lt;code&gt;write.target-file-size-bytes&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reads slow after many &lt;code&gt;MERGE&lt;/code&gt;s&lt;/td&gt;
&lt;td&gt;Merge-on-read delete files piling up&lt;/td&gt;
&lt;td&gt;Compact with a delete-file threshold; run &lt;code&gt;rewrite_position_delete_files&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Query planning takes seconds before any data is read&lt;/td&gt;
&lt;td&gt;Too many snapshots or manifests&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;expire_snapshots&lt;/code&gt;, &lt;code&gt;rewrite_manifests&lt;/code&gt;, cap &lt;code&gt;write.metadata.previous-versions-max&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S3 bill grows while the table doesn't&lt;/td&gt;
&lt;td&gt;Unexpired snapshots; orphan files from failed jobs&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;expire_snapshots&lt;/code&gt;, then &lt;code&gt;remove_orphan_files&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A filter scans the whole partition anyway&lt;/td&gt;
&lt;td&gt;Filtering on a value inside a map or JSON column, or on a function of a column&lt;/td&gt;
&lt;td&gt;Promote the field to a real column; compare raw columns to literals&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Millions of tiny partitions&lt;/td&gt;
&lt;td&gt;Partitioning on a high-cardinality column (user, day for a quiet table)&lt;/td&gt;
&lt;td&gt;Use coarser transforms like &lt;code&gt;months(ts)&lt;/code&gt; or &lt;code&gt;bucket(16, id)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Commit retries and timeouts under load&lt;/td&gt;
&lt;td&gt;Many concurrent writers on one table&lt;/td&gt;
&lt;td&gt;Fewer, larger commits; separate tables for separate write paths&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  Faster queries
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. Partition by how you filter and keep partitions big
&lt;/h3&gt;

&lt;p&gt;Partition on the two or three columns almost every query filters on. Aim for each partition to hold at least a few hundred MB. Iceberg's &lt;strong&gt;hidden partitioning&lt;/strong&gt; means you declare a transform once and users never see it:&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transactions&lt;/span&gt; &lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;iceberg&lt;/span&gt;
&lt;span class="n"&gt;PARTITIONED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;account_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;months&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transaction_date&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;-- Users filter on the real column; Iceberg maps it to the month partition.&lt;/span&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transactions&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;account_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;transaction_date&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="s1"&gt;'2026-09-01'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;transaction_date&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;  &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="s1"&gt;'2026-10-01'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Got it wrong? Partition specs can evolve without rewriting old data: &lt;code&gt;ALTER TABLE db.transactions ADD PARTITION FIELD bucket(16, merchant_id)&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Sort inside partitions so min/max statistics mean something
&lt;/h3&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdgxh55lidnmix9wgje6a.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdgxh55lidnmix9wgje6a.png" alt=" " width="800" height="344"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Figure 12.&lt;/strong&gt; Each bar is one file's min-to-max range for &lt;code&gt;merchant_id&lt;/code&gt;. Iceberg can only skip a file when the value you want falls outside its range, so random order makes the statistics useless.&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;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transactions&lt;/span&gt; &lt;span class="k"&gt;WRITE&lt;/span&gt; &lt;span class="n"&gt;ORDERED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;merchant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source_reference&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Compaction then re-sorts existing data:&lt;/span&gt;
&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="k"&gt;catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rewrite_data_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'db.transactions'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;strategy&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'sort'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  A worked example: our four-column sort order
&lt;/h3&gt;

&lt;p&gt;Our transactions table is partitioned by &lt;code&gt;(client_id, account_id, months(transaction_date))&lt;/code&gt; and declares this sort order:&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;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transactions&lt;/span&gt;
&lt;span class="k"&gt;WRITE&lt;/span&gt; &lt;span class="n"&gt;ORDERED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;transaction_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;account_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reporting_tag&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It reads like clustering on four columns. In practice it clusters on one. Partitioning happens before sorting and every data file belongs to exactly one partition. So inside any file, &lt;code&gt;client_id&lt;/code&gt; and &lt;code&gt;account_id&lt;/code&gt; never change and sorting by a constant does nothing. &lt;code&gt;transaction_date&lt;/code&gt; is a timestamp and almost unique, so &lt;code&gt;reporting_tag&lt;/code&gt; only orders rows that share the exact same timestamp.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhg9msgitp0o65eistaj4.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhg9msgitp0o65eistaj4.png" alt=" " width="800" height="270"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Figure 13.&lt;/strong&gt; What the sort order does once partitioning has run. Only &lt;code&gt;transaction_date&lt;/code&gt; changes the order of rows inside a file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb:&lt;/strong&gt; a sort column only helps if it still varies inside a partition and if the columns before it repeat often enough to leave ties for it to break.&lt;/p&gt;

&lt;p&gt;To see what this means for queries, take one partition (one client, one account, October) holding six files and two filters our reconciliation rules use all the time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Query A&lt;/strong&gt;, one day, every tag: &lt;code&gt;transaction_date &amp;gt;= TIMESTAMP '2026-10-12' AND transaction_date &amp;lt; TIMESTAMP '2026-10-13'&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Query B&lt;/strong&gt;, one tag, the whole month: &lt;code&gt;reporting_tag = 'chargebacks'&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each rectangle below is one file's min/max box: its date range across, its tag range down. Iceberg reads a file only if the query's line or band touches its box.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F91r8ur7lc75bgr7uxplv.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F91r8ur7lc75bgr7uxplv.png" alt=" " width="800" height="281"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Figure 14.&lt;/strong&gt; Rows written in sort order. Files become date stripes, so date filters skip well. Every stripe spans the alphabet of tags, from &lt;code&gt;CardPayments&lt;/code&gt; to &lt;code&gt;withdrawals&lt;/code&gt;, so a tag filter skips nothing.&lt;/p&gt;

&lt;p&gt;Then compaction changes the picture. Our nightly job rewrites the last three months with &lt;code&gt;strategy =&amp;gt; 'sort', sort_order =&amp;gt; 'zorder(transaction_date, client_id, account_id, reporting_tag)'&lt;/code&gt;. A &lt;code&gt;sort_order&lt;/code&gt; passed to &lt;code&gt;rewrite_data_files&lt;/code&gt; overrides the table's own order for the files it writes. Z-order interleaves the bits of each column, so each file ends up covering a block of dates &lt;em&gt;and&lt;/em&gt; a block of tags. The constant partition columns drop out again, which leaves date and tag sharing the ordering.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frneagyie8akt8hof6qsh.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frneagyie8akt8hof6qsh.png" alt=" " width="800" height="281"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Figure 15.&lt;/strong&gt; The same partition after z-order compaction. The dotted line is the order the curve visits the blocks. Date skipping gets weaker and tag skipping starts to work.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Filter&lt;/th&gt;
&lt;th&gt;Linear by date&lt;/th&gt;
&lt;th&gt;Z-order (date, tag)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A: one day, every tag&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;1 of 6&lt;/strong&gt; files&lt;/td&gt;
&lt;td&gt;3 of 6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B: one tag, whole month&lt;/td&gt;
&lt;td&gt;6 of 6&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2 of 6&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A and B together&lt;/td&gt;
&lt;td&gt;1 of 6&lt;/td&gt;
&lt;td&gt;1 of 6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;client_id&lt;/code&gt; + &lt;code&gt;account_id&lt;/code&gt; + month only&lt;/td&gt;
&lt;td&gt;Partition pruning does all the work. Sort order doesn't matter.&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;There is one more layer. With a 512 MB target file size, a single account's month often fits in one to three files, so file-level skipping has little to choose from. The sort still pays off inside the file. Parquet stores min/max per row group and sorted rows give each row group a narrow date range.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyh6oft7kjq4buo0cidz6.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyh6oft7kjq4buo0cidz6.png" alt=" " width="799" height="287"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Figure 16.&lt;/strong&gt; Inside one file. Even when a partition is a single file, sorting by date lets the reader skip most of it. Unsorted, it reads everything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What we took from this:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Partition columns in the sort order do nothing.&lt;/strong&gt; Listing &lt;code&gt;client_id&lt;/code&gt; and &lt;code&gt;account_id&lt;/code&gt; is harmless, but it suggests clustering that isn't happening.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A column behind a timestamp in a linear sort does nothing.&lt;/strong&gt; If tag filters matter, either z-order on date and tag, or put the low-cardinality column first: &lt;code&gt;WRITE ORDERED BY reporting_tag, transaction_date&lt;/code&gt; gives each tag its own date-sorted run. The cost is that a one-day, every-tag query then touches one run per tag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't let two layouts drift apart.&lt;/strong&gt; New data lands linear by date. Compaction later turns it into z-order. Choose one layout on purpose, based on which filter your queries use most and set it in both places.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The sort only helps filters on the raw column.&lt;/strong&gt; &lt;code&gt;CAST(transaction_date AS DATE)&lt;/code&gt; or &lt;code&gt;DATE_FORMAT(...)&lt;/code&gt; can hide the range from the planner (see tip 5 below).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the layout, don't guess it.&lt;/strong&gt; In Spark, the &lt;code&gt;files&lt;/code&gt; metadata table shows each file's ranges:
&lt;/li&gt;
&lt;/ul&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;file_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;record_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;readable_metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transaction_date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lower_bound&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;date_min&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;readable_metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transaction_date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;upper_bound&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;date_max&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;readable_metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reporting_tag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lower_bound&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;tag_min&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;readable_metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reporting_tag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;upper_bound&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;tag_max&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transactions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;partition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;partition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;account_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;date_min&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Narrow, non-overlapping date ranges: date skipping works.&lt;/span&gt;
&lt;span class="c1"&gt;-- tag_min = 'CardPayments' and tag_max = 'withdrawals' on every file: tag skipping doesn't.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  3. Don't hide hot fields inside maps or JSON
&lt;/h3&gt;

&lt;p&gt;A &lt;code&gt;MAP&amp;lt;STRING,STRING&amp;gt;&lt;/code&gt; column is handy for odd extra fields. But Parquet stores every map key in one column and every value in another, so the min/max statistics mix all keys together.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvnewm4n2zcuxzxythqaf.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvnewm4n2zcuxzxythqaf.png" alt=" " width="799" height="324"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Figure 17.&lt;/strong&gt; Same data, two layouts. The map version must read and rebuild the whole map for every row. The promoted column lets Iceberg skip the file from metadata alone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How a map column behaves in each of the three skipping layers:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Partition pruning still works,&lt;/strong&gt; as long as the query also filters on partition columns. That is the only layer left.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File min/max is useless.&lt;/strong&gt; The "metadata values" range covers &lt;code&gt;rrn&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;, &lt;code&gt;amount&lt;/code&gt; and every other key mixed together, so no file can ever be ruled out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Row-group statistics are useless too,&lt;/strong&gt; for the same reason. Engines like Athena (Trino) and Spark don't push a map lookup into the scan at all. They read the column, rebuild the map for each row, then filter.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Top-level typed column&lt;/th&gt;
&lt;th&gt;Key inside a map&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Files skipped by min/max&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Bytes read&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Only that column&lt;/td&gt;
&lt;td&gt;The entire map, all keys and values, for every row in the scanned partitions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CPU&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Compare values directly&lt;/td&gt;
&lt;td&gt;Rebuild the map per row, look up the key, often &lt;code&gt;CAST&lt;/code&gt; the string&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;As a join key&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The engine can use the other side's values to skip files on this side (dynamic filtering)&lt;/td&gt;
&lt;td&gt;Can't. &lt;code&gt;ON element_at(l.metadata, 'rrn') = r.x&lt;/code&gt; is an expression, so every row is evaluated&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A map is often one of the widest columns in the table, so reading all of it to check one key can be most of a query's scan.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What to do about it:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Promote the keys you filter or join on&lt;/strong&gt; to top-level columns. In Iceberg, &lt;code&gt;ALTER TABLE ... ADD COLUMNS&lt;/code&gt; only changes metadata, so it's cheap. Fill the new column at ingestion, backfill old rows once with an &lt;code&gt;UPDATE&lt;/code&gt; or &lt;code&gt;MERGE&lt;/code&gt; and keep the map for rare keys.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Give them real types,&lt;/strong&gt; for example amounts as &lt;code&gt;decimal(32,4)&lt;/code&gt;, so min/max statistics and comparisons are correct.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add Bloom filters&lt;/strong&gt; for exact lookups on high-cardinality columns like IDs: &lt;code&gt;'write.parquet.bloom-filter-enabled.column.rrn'='true'&lt;/code&gt;. Check that your engine version actually reads them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rewrite the queries&lt;/strong&gt; to use the new columns. Filters and join keys then get file skipping and dynamic filtering for free.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  4. Joins: cheap when pruned, expensive when not
&lt;/h3&gt;

&lt;p&gt;A join isn't slow by itself. On a data lake, its cost comes from three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bytes scanned on each side.&lt;/strong&gt; If both tables are filtered on their partition columns, each side is already small before the join starts. Put the partition filters on every table in the join, not just one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The join key.&lt;/strong&gt; On a plain column, Athena and Trino build a filter from the smaller side's keys and use it to skip files on the bigger side. A map lookup, &lt;code&gt;CAST&lt;/code&gt; or function in the &lt;code&gt;ON&lt;/code&gt; clause turns that off.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rows exploded before the join.&lt;/strong&gt; &lt;code&gt;UNNEST&lt;/code&gt; and &lt;code&gt;CROSS JOIN UNNEST(...)&lt;/code&gt; on arrays multiply rows before the join and can cost more than the join itself. Filter first, unnest last.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So joins on well-pruned plain columns are fine. A join keyed on map values over a wide date range is where queries get slow and it's slow for the map reasons above, not because it's a join.&lt;/p&gt;
&lt;h3&gt;
  
  
  5. Compare raw columns to literals
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;transaction_date &amp;gt;= TIMESTAMP '2026-09-01'&lt;/code&gt; prunes reliably. Wrapping the column in a function or cast, as in &lt;code&gt;CAST(transaction_date AS date) = ...&lt;/code&gt;, may not, depending on the engine. Put the transformation on the literal side.&lt;/p&gt;
&lt;h2&gt;
  
  
  Cheaper, leaner storage
&lt;/h2&gt;

&lt;p&gt;Iceberg never deletes anything by itself. Every rewrite leaves the old files in place for old snapshots. You need a maintenance routine and the order matters.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fln5xun3dn90zfyhbyc2h.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fln5xun3dn90zfyhbyc2h.png" alt=" " width="799" height="254"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Figure 18.&lt;/strong&gt; Compaction alone makes queries faster but storage bigger. Storage only shrinks once expired snapshots release the old files.&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="c1"&gt;-- Spark (Iceberg procedures)&lt;/span&gt;
&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="k"&gt;catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rewrite_data_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'db.transactions'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'transaction_date &amp;gt;= current_date() - INTERVAL 7 DAYS'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="k"&gt;catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rewrite_manifests&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'db.transactions'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="k"&gt;catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expire_snapshots&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'db.transactions'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;older_than&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="s1"&gt;'2026-10-22 00:00:00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retain_last&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="k"&gt;catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;remove_orphan_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'db.transactions'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Athena equivalents&lt;/span&gt;
&lt;span class="n"&gt;OPTIMIZE&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transactions&lt;/span&gt; &lt;span class="n"&gt;REWRITE&lt;/span&gt; &lt;span class="k"&gt;DATA&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;BIN_PACK&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;VACUUM&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transactions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Settings that shrink the bill
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Suggested value&lt;/th&gt;
&lt;th&gt;Effect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;write.parquet.compression-codec&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zstd&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Usually smaller files than snappy for similar read speed. It's the default in recent Iceberg versions.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;write.target-file-size-bytes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;128–512 MB&lt;/td&gt;
&lt;td&gt;Fewer files, fewer S3 requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;write.metadata.delete-after-commit.enabled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Old &lt;code&gt;metadata.json&lt;/code&gt; files are cleaned up automatically&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;write.metadata.previous-versions-max&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10–50&lt;/td&gt;
&lt;td&gt;Caps how many old metadata files are kept&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;write.metadata.metrics.default&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;truncate(16)&lt;/code&gt;, with &lt;code&gt;none&lt;/code&gt; for wide text columns you never filter on&lt;/td&gt;
&lt;td&gt;Smaller manifests on wide tables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Column types&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;decimal&lt;/code&gt;, &lt;code&gt;date&lt;/code&gt;, &lt;code&gt;bigint&lt;/code&gt; instead of &lt;code&gt;string&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Better compression and min/max that compares correctly&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  What map columns cost in storage
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Key names are stored on every row.&lt;/strong&gt; The strings &lt;code&gt;"rrn"&lt;/code&gt;, &lt;code&gt;"status"&lt;/code&gt; and so on repeat for each row. Parquet's dictionary encoding and compression shrink them well because they repeat, so this overhead is real but modest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Values compress worse than typed columns.&lt;/strong&gt; Everything is a &lt;code&gt;STRING&lt;/code&gt;, so an amount is stored as text instead of a &lt;code&gt;decimal&lt;/code&gt;. Mixing many keys in one column also makes a messier dictionary than separate columns would.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The bigger cost is reading, not storing.&lt;/strong&gt; Athena bills per byte scanned and every filter on a map key scans the whole map.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Simpler queries, easier changes
&lt;/h2&gt;

&lt;p&gt;Performance tuning is wasted if nobody can safely change the queries afterwards. These habits kept our SQL short and our migrations boring.&lt;/p&gt;

&lt;h3&gt;
  
  
  Typed columns beat map lookups in readability too
&lt;/h3&gt;

&lt;p&gt;Before&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;transactions&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;element_at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'ReconAmount'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'rrn'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'reference'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After&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;transactions&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;recon_amount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rrn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second version is shorter, prunes files and fails loudly at write time if the data is the wrong type, not silently at query time.&lt;/p&gt;

&lt;h3&gt;
  
  
  More habits
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lean on hidden partitioning.&lt;/strong&gt; Never ask users to add a separate &lt;code&gt;month = '2026-09'&lt;/code&gt; filter. They filter on the timestamp; Iceberg does the rest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evolve schemas freely, but never reuse a column name for new meaning.&lt;/strong&gt; Iceberg tracks columns by ID, so add, rename and drop are safe metadata changes. Readers of old dashboards are not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One table per write path, views for reading.&lt;/strong&gt; Separate tables keep writers from colliding (Figure 3). A &lt;code&gt;UNION ALL&lt;/code&gt; view gives analysts one place to query and engines push filters down into it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debug with time travel.&lt;/strong&gt; &lt;code&gt;SELECT ... FOR VERSION AS OF 4129...&lt;/code&gt; shows the table before a bad job. &lt;code&gt;CALL catalog.system.rollback_to_snapshot(...)&lt;/code&gt; undoes it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep table properties in code.&lt;/strong&gt; One &lt;code&gt;CREATE TABLE&lt;/code&gt; template, applied to every environment, stops tables drifting apart.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  One table or three?
&lt;/h2&gt;

&lt;p&gt;We keep transactions in three tables, one for each source: &lt;code&gt;bank&lt;/code&gt; statements from banks and payment providers, our own &lt;code&gt;ledger&lt;/code&gt; and &lt;code&gt;external&lt;/code&gt; for other third-party feeds. They share one schema. Every so often someone asks whether one &lt;code&gt;transactions&lt;/code&gt; table with a &lt;code&gt;kind&lt;/code&gt; column would be simpler. It's a fair question and the answer depends on how Iceberg commits, rolls back and prunes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tables, not buckets:&lt;/strong&gt; all three tables live in the same S3 bucket under one warehouse path. In Iceberg, the unit that matters is the &lt;em&gt;table&lt;/em&gt;, meaning its own metadata tree and its own commit pointer. Whether those files sit in one bucket or three changes access policies, lifecycle rules and cost reports, not query speed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Writes: one pointer per table
&lt;/h3&gt;

&lt;p&gt;Each table has exactly one "current metadata" pointer and every commit swaps it. Writers to &lt;em&gt;different&lt;/em&gt; tables never wait on each other. Writers to the &lt;em&gt;same&lt;/em&gt; table take turns: the loser re-reads the latest metadata, checks for conflicts and tries again (see the commit figure in "How writes commit").&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft7sp9apwxfiyrm1zxkav.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft7sp9apwxfiyrm1zxkav.png" alt=" " width="800" height="305"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Figure 23.&lt;/strong&gt; Each source has its own ingestion jobs, reconciliation &lt;code&gt;MERGE&lt;/code&gt;s and compaction. Split into three tables, those commits run side by side. Merged into one, every commit from every source queues on one pointer.&lt;/p&gt;

&lt;p&gt;A retry is usually cheap when the writers touched different partitions, because only metadata is rewritten. But retries add latency, they can run out (we already raised &lt;code&gt;commit.retry.num-retries&lt;/code&gt; to 20) and a &lt;code&gt;MERGE&lt;/code&gt; that hits a real conflict fails after doing all its work. Merging three tables roughly triples the commit traffic on the one pointer that's left.&lt;/p&gt;
&lt;h3&gt;
  
  
  Mistakes: the rollback blast radius
&lt;/h3&gt;

&lt;p&gt;Rollback works per table. &lt;code&gt;rollback_to_snapshot&lt;/code&gt; moves the table's pointer back, so every commit after that point disappears, whichever source wrote it.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0qmqj5ooo5appsfj9slc.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0qmqj5ooo5appsfj9slc.png" alt=" " width="800" height="340"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Figure 24.&lt;/strong&gt; A bad ledger load lands at 10:00. With separate tables, undoing it touches only the ledger table. With one table, the rollback also erases the reconciliation results and the ingest that other sources committed afterwards.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-- Three tables: undo only the bad ledger load.
CALL catalog.system.rollback_to_snapshot('db.transactions_ledger', 8812734455019);

-- One table: there is no "undo only ledger" snapshot to roll back to.
-- You either roll everything back and rerun the other sources, or write a targeted DELETE.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Reads: a draw, if you partition right
&lt;/h3&gt;

&lt;p&gt;For reads, the layout matters more than the table count. One table with &lt;code&gt;kind&lt;/code&gt; as the &lt;em&gt;first&lt;/em&gt; partition field prunes to exactly the same files as three tables. The risks come from getting that layout wrong, or forgetting the filter.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F502lvven9hfzt451xhb3.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F502lvven9hfzt451xhb3.png" alt=" " width="799" height="252"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Figure 25.&lt;/strong&gt; Data read by a query for bank rows only. Separate tables make the filter impossible to forget, because it's the table name.&lt;/p&gt;

&lt;p&gt;Cross-source queries cost the same either way. Matching bank rows to ledger rows reads bank files and ledger files whether they come from two tables or from two partitions of one table.&lt;/p&gt;
&lt;h3&gt;
  
  
  The trade-offs side by side
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Three tables&lt;/th&gt;
&lt;th&gt;One table with &lt;code&gt;kind&lt;/code&gt;
&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Commit contention&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Isolated per source&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;All sources share one pointer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rollback and time travel&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Per source&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;All sources together&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read performance&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;Same, only if &lt;code&gt;kind&lt;/code&gt; is the first partition field&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Forgetting the source filter&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Impossible&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Silent: 3× the data and mixed results&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maintenance&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Tuned per table&lt;/strong&gt;; a big source never delays a small one&lt;/td&gt;
&lt;td&gt;One larger job; per-source tuning needs &lt;code&gt;where&lt;/code&gt; filters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schema changes&lt;/td&gt;
&lt;td&gt;Can differ per source, but must be kept in sync by hand&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;One schema to change&lt;/strong&gt;; source-only columns are null elsewhere&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Access control&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Grant per table&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Needs row-level filters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Querying all sources&lt;/td&gt;
&lt;td&gt;Needs a &lt;code&gt;UNION ALL&lt;/code&gt; view&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;One name&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Number of objects to run&lt;/td&gt;
&lt;td&gt;3 tables, 3 maintenance jobs, 3 sets of DDL&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1 of each&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage cost&lt;/td&gt;
&lt;td&gt;The same. Bytes don't care which table they're in.&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Separate tables win when&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;each source has its own ingestion schedule and writers&lt;/li&gt;
&lt;li&gt;heavy &lt;code&gt;MERGE&lt;/code&gt;s run per source&lt;/li&gt;
&lt;li&gt;volumes differ a lot between sources&lt;/li&gt;
&lt;li&gt;you need to undo one source without touching the rest&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One table wins when&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;there are many small sources (thirty tables is real overhead)&lt;/li&gt;
&lt;li&gt;one job writes every source in a single commit&lt;/li&gt;
&lt;li&gt;most queries span all sources&lt;/li&gt;
&lt;li&gt;schemas are identical and stable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our sources match the left column, so we keep three tables. A view gives people who want "all transactions" a single name to query, without bringing back shared commits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CREATE VIEW db.transactions_all AS
SELECT 'bank'     AS kind, * FROM db.transactions_bank
UNION ALL
SELECT 'ledger'   AS kind, * FROM db.transactions_ledger
UNION ALL
SELECT 'external' AS kind, * FROM db.transactions_external;

-- kind is a constant in each branch, so the engine drops the branches that can't match:
SELECT count(*) FROM db.transactions_all WHERE kind = 'ledger';   -- reads only the ledger table
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Rule of thumb:&lt;/strong&gt; split tables along the lines where writes are independent. Join them back with views for reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Coming from a relational database
&lt;/h2&gt;

&lt;p&gt;If you've spent years tuning Postgres, MySQL or SQL Server, most of your instincts still apply. SQL is SQL. But a few habits that served you well there work against you on a lake. The shift underneath all of them: &lt;strong&gt;a database owns its storage and keeps it tidy for you; a lake is files on S3 and you design and maintain the layout yourself.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  1. "Add an index" becomes "design the layout"
&lt;/h3&gt;

&lt;p&gt;Iceberg has no secondary indexes. Nothing points from a value to a row. Speed comes from not reading data at all: partitioning, sort order and min/max statistics let the engine skip files and row groups. The layout &lt;em&gt;is&lt;/em&gt; the index and you pick it once, for your most common filters.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbbxmdcu4wcfdf1k4t7hc.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbbxmdcu4wcfdf1k4t7hc.png" alt=" " width="800" height="317"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Figure 26.&lt;/strong&gt; A database seeks to the row. A lake skips everything it can prove irrelevant, then scans what's left. Point lookups on a column nobody sorted or partitioned by are a full scan.&lt;/p&gt;

&lt;p&gt;So the question "why is this lookup slow?" has a different answer. In a database you'd check for a missing index. On a lake you check whether the filter column is a partition column, a sort column, or neither.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. "&lt;code&gt;SELECT *&lt;/code&gt; is harmless" becomes "every column costs money"
&lt;/h3&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fekg7a6u1nlr2mjhulkrw.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fekg7a6u1nlr2mjhulkrw.png" alt=" " width="800" height="262"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Figure 27.&lt;/strong&gt; Parquet is a column store. A query reads only the columns it names, so query cost grows with the number of columns, not just rows. Athena bills by bytes scanned, so a lazy &lt;code&gt;SELECT *&lt;/code&gt; on a wide table shows up on the invoice.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. "Update a row" becomes "write a new version of the table"
&lt;/h3&gt;

&lt;p&gt;Database pages change in place. Lake files never change. Every &lt;code&gt;UPDATE&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt; or &lt;code&gt;MERGE&lt;/code&gt; writes new files and commits a new snapshot, however few rows it touches.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fisrrt27fd1sxlhpezgkd.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fisrrt27fd1sxlhpezgkd.png" alt=" " width="800" height="305"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Figure 28.&lt;/strong&gt; The fixed cost of a statement is files and a commit, not a page. Row-by-row loops, which a database shrugs off, are the most expensive way to change a lake. Batch the change into one set-based &lt;code&gt;MERGE&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  More habits to unlearn
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Relational habit&lt;/th&gt;
&lt;th&gt;Lake mindset&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Normalize; joins are cheap&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Wide tables are normal&lt;/strong&gt;; join only on pruned inputs&lt;/td&gt;
&lt;td&gt;A join of two big tables shuffles data read from S3 across the cluster. Without a selective filter on each side, both sides are read in full.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stash extras in a JSON column and index it&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Promote hot fields to typed columns&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;There's no GIN index. A value inside a map or JSON blob can't be skipped and the whole blob is read for every row.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wrap columns in functions; an expression index covers it&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Compare bare columns to literals&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No expression indexes either. A function around a partition column can hide it from pruning and that's how we once scanned 130 GB (Figure 14).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;PRIMARY KEY&lt;/code&gt; and &lt;code&gt;UNIQUE&lt;/code&gt; keep data clean&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Your pipeline keeps data clean&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Iceberg doesn't enforce keys, uniqueness or foreign keys. Inserting the same row twice gives you two rows. Deduplicate in a &lt;code&gt;MERGE&lt;/code&gt; or in the load job.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Autovacuum tidies up&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Schedule maintenance yourself&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Compaction, snapshot expiry and orphan removal are jobs you own. Skip them and queries slow down while storage grows.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deleted rows free space&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Space frees when snapshots expire&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Old snapshots still reference old files for time travel. Until they expire, a &lt;code&gt;DELETE&lt;/code&gt; makes storage grow, not shrink.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Thousands of small concurrent writes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Few, large writes per table&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Each commit swaps one pointer per table, so concurrent writers take turns. Small writes also leave small files behind.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A transaction can span tables&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Atomic per table only&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Each table commits on its own. Make every step idempotent so a failed run can simply be rerun.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage is the expensive part&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Scans are the expensive part&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;S3 storage is cheap per GB. What you pay for is bytes scanned and compute time, so layout and filters matter more than disk size.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The planner will sort it out&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Check "data scanned" on every new query&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The optimizer can only skip what the layout and filters allow. The bytes-scanned number tells you at once whether pruning worked.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Things you can stop worrying about
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Schema changes are instant.&lt;/strong&gt; Adding, renaming or dropping a column is a metadata change. No table lock, no rewrite, no migration window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Readers never block writers.&lt;/strong&gt; A query reads one fixed snapshot while new commits land next to it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time travel is built in.&lt;/strong&gt; &lt;code&gt;FOR TIMESTAMP AS OF&lt;/code&gt; or a snapshot ID answers "what did this look like yesterday?" without a backup restore.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can change the partitioning later.&lt;/strong&gt; New data uses the new spec and old data stays readable as it is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage grows with you.&lt;/strong&gt; There's no disk to resize and no tablespace to fill.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The mindset in one line:&lt;/strong&gt; design the layout around your filters, read as few bytes as possible, write in batches and treat maintenance as part of the system, not an afterthought.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Partitions match the filters in most queries and hold at least a few hundred MB each&lt;/li&gt;
&lt;li&gt;A sort order is set on the columns you look up by&lt;/li&gt;
&lt;li&gt;Fields you filter or join on are real typed columns, not map keys or JSON paths&lt;/li&gt;
&lt;li&gt;Merge-on-read tables are compacted at least daily&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;expire_snapshots&lt;/code&gt; and &lt;code&gt;remove_orphan_files&lt;/code&gt; run on a schedule, in that order&lt;/li&gt;
&lt;li&gt;Writes are batched; independent write paths use separate tables&lt;/li&gt;
&lt;li&gt;Compression is &lt;code&gt;zstd&lt;/code&gt; and target file size is 128–512 MB&lt;/li&gt;
&lt;li&gt;Table DDL and properties live in version control&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Iceberg makes a data lake behave like a database, but it doesn't clean up after you. Design the layout around how you query, keep the metadata tidy and the lake stays fast and cheap as it grows.&lt;/p&gt;

</description>
      <category>dataengineering</category>
    </item>
    <item>
      <title>When Your Codebase Blocks Agile</title>
      <dc:creator>Josephat Macharia</dc:creator>
      <pubDate>Fri, 22 Aug 2025 13:23:03 +0000</pubDate>
      <link>https://dev.to/mashaa/when-your-codebase-blocks-agile-36d8</link>
      <guid>https://dev.to/mashaa/when-your-codebase-blocks-agile-36d8</guid>
      <description>&lt;p&gt;One of the biggest mistakes companies make when they adopt Agile is assuming that changing the process is enough. Daily stand-ups, sprints and Kanban boards are introduced — but the code remains untouched, structured for a world that predates Agile.&lt;/p&gt;

&lt;p&gt;The result? An architecture mismatch that quietly undermines agility. Teams may be “Agile” in ceremony, but in practice, they are slowed down so much that they can hardly respond to change in any meaningful sense.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Hidden Bottleneck&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Agility is about responding quickly to new requirements, shifting priorities and customer needs. But if your codebase is still organized in a way that resists change, every new feature feels like a battle. Instead of building, teams spend disproportionate time:&lt;/p&gt;

&lt;p&gt;Untangling tightly coupled modules&lt;/p&gt;

&lt;p&gt;Reworking brittle dependencies&lt;/p&gt;

&lt;p&gt;Fighting against legacy design decisions&lt;/p&gt;

&lt;p&gt;This isn’t a process problem. It’s an architectural problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Architecture Shapes Agility&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A system’s architecture sets the boundaries for speed, adaptability and collaboration. If the architecture reflects an older way of working — where long release cycles and rigid silos were the norm — Agile practices will only expose those weaknesses faster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agile thrives when the codebase itself is:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Modular&lt;/strong&gt; – enabling teams to work independently without constant conflicts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Flexible&lt;/strong&gt; – designed for extensions and iterations, not rigid perfection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Evolvable&lt;/strong&gt; – allowing refactoring and improvement as part of everyday work.&lt;/p&gt;

&lt;p&gt;Without these qualities, your Agile transformation risks becoming Agile in name only.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Road Ahead&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you are considering a move to Agile, be prepared: &lt;strong&gt;restructuring the fundamental architecture of your system is almost certainly in your future&lt;/strong&gt;. The sooner you address it, the faster your teams can deliver value without being slowed down by the very code they’re meant to improve.&lt;/p&gt;

&lt;p&gt;Agility isn’t just about process. It’s about aligning &lt;strong&gt;process and architecture&lt;/strong&gt; so that your teams — and your software — can truly move at the speed of change.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Rethinking Estimation In Agile Development</title>
      <dc:creator>Josephat Macharia</dc:creator>
      <pubDate>Thu, 15 May 2025 18:35:50 +0000</pubDate>
      <link>https://dev.to/mashaa/rethinking-estimation-in-agile-development-4d17</link>
      <guid>https://dev.to/mashaa/rethinking-estimation-in-agile-development-4d17</guid>
      <description>&lt;h1&gt;
  
  
  Estimation
&lt;/h1&gt;

&lt;p&gt;Perhaps we should rethink and reassess our ritual of story estimation.&lt;br&gt;&lt;br&gt;
Direct from the Scrum creator himself:&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%2Fixlwivh46bxoi4f63bqv.jpeg" 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%2Fixlwivh46bxoi4f63bqv.jpeg" alt=" " width="396" height="286"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Most story estimation is a waste of everybody's time and a recipe for organisational disappointment.&lt;/p&gt;

&lt;p&gt;One of the ongoing debates in agile circles is that of story sizing. For years, it has seemed to be a settled topic, but there are always new people entering the field that don't know that it's settled, go out looking for how long something is going to take and find a bunch of information that says you should use story points (or t-shirt sizing, or something similar) to figure out how big something is and get an idea of how long it will take.&lt;/p&gt;

&lt;p&gt;So, let's see if we can bring people up to speed. When XP was developed, its creators thought it was important to be able to have an indicative value of the relative complexity of work. &lt;br&gt;
They started with ideal hours, then padded them and then settled on points to abstract away from time, because they found that sizing by hours didn't work very well. Points were never meant to be a predictor of duration.&lt;/p&gt;

&lt;p&gt;After a number of years, just about everybody involved in formalising story points realised it didn't work particularly well, for lots of reasons and suggested we move to something else.&lt;/p&gt;

&lt;p&gt;Here's Ron Jeffries on how his views of story points have changed:&lt;br&gt;
"I like to say that I may have invented story points and if I did, I’m sorry now."&lt;br&gt;
From Story Points Revisited&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%2Fn6w7504t0lxi5saaamc6.jpeg" 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%2Fn6w7504t0lxi5saaamc6.jpeg" alt=" " width="800" height="122"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For a lot of people, that isn't enough. Knowing that the creators thought it was a mistake isn't enough to enable people to change.&lt;br&gt;
To change, people also need to know what to change to. For that, a detailed breakdown of what to do instead is really valuable. So, here it is:&lt;/p&gt;

&lt;p&gt;Why estimating doesn't work:&lt;br&gt;
✔ Parkinson's Law (the idea that work expands to fill the time allotted for its completion. This may mean you take longer than necessary to complete a task or you procrastinate and complete the task right before the due date.)&lt;br&gt;
✔ Optimism&lt;br&gt;
✔ Inherent uncertainty of creative work (This uncertainty stems from the open-ended nature of creative challenges, where there is rarely a single "right" answer or a clear, pre-defined path to a successful outcome. )&lt;br&gt;
✔ Humans are awful at estimating anything bigger than about 2 days of work&lt;/p&gt;

&lt;p&gt;What to do instead:&lt;br&gt;
💡 Make stories smaller. No, even smaller. (Hint: See "2 days" above)&lt;br&gt;
💡 Actively seek to understand the uncertainty&lt;br&gt;
💡 Use exploratory stories&lt;br&gt;
💡 Write genuine user stories&lt;br&gt;
💡 Remember that user stories are a placeholder for a conversation&lt;br&gt;
💡 Go talk to the user&lt;br&gt;
💡 Utilizes short iterations of &lt;strong&gt;1-2 weeks&lt;/strong&gt; while embracing continuous releases.&lt;br&gt;
💡 Incorporates sprint planning and review while allowing flexibility and no commitment.&lt;br&gt;
💡 No notion of story points or “points per sprint.”&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fallacy of Estimates
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Estimates are inherently based on guesswork.&lt;/li&gt;
&lt;li&gt;Estimation leads to dysfunction and false commitments.&lt;/li&gt;
&lt;li&gt;It's not that things are late 80% of the time; it's that we don't know how to do estimates 80% of the time, so our estimates are always wrong.&lt;/li&gt;
&lt;li&gt;Making decisions based on erroneous estimations leads to significant process breakdown.&lt;/li&gt;
&lt;li&gt;Story points have no meaning, any more than time-based estimates would have.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Velocity
&lt;/h1&gt;

&lt;p&gt;What if you need to understand velocity? Simple: count the stories. That's your velocity. Once you make stories really small, their variability decreases and you get through more. The Law of Large Numbers will help eliminate some of the variability you used to see in your work, so counting stories becomes a perfectly reasonable way to measure velocity, if you need to (you probably don't).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Velocity is a ratio that compares the hypothetical time needed to complete a project without interruptions to the actual time it takes in practice.&lt;/li&gt;
&lt;li&gt;Essentially, it's a decimal figure ranging from 0 to 1, similar to a percentage.&lt;/li&gt;
&lt;li&gt;Our wrong estimates are tied into this.&lt;/li&gt;
&lt;li&gt;We wrongly attribute decreased velocity to team inefficiency rather than acknowledging systemic barriers (e.g., bureaucratic processes and unnecessary tasks).&lt;/li&gt;
&lt;li&gt;Velocity is another destructive force, often misinterpreted as a measure of efficiency or productivity.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Sprint "Spillover"
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;In Agile methodology, there's no concept of Sprint "spillover."&lt;/li&gt;
&lt;li&gt;Agile embraces change, even during coding, allowing for feedback-driven adjustments.&lt;/li&gt;
&lt;li&gt;Insisting on estimation often means misunderstanding Agile, resorting to what is termed as "fake Scrum."&lt;/li&gt;
&lt;li&gt;True Agile doesn't require committing to a fixed set of stories within a Sprint; instead, it focuses on committing to overall goals, emphasizing adaptability over rigid planning, similar to traditional waterfall approaches.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Some people will say, "Aha! You said estimating was a waste, but you must estimate in order to figure out if a story is less than 2 days." You're right. But that's the extent of estimation's value. If you understand what's needed well enough, you'll be able to get the story to be smaller than 2 days. If you don't, you won't.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It's time to move away from the culture of estimates and focus on delivering valuable software.&lt;/li&gt;
&lt;li&gt;If we are obsessed with estimates, let's employ a difficulty scale that is difficult to translate into time.

&lt;ul&gt;
&lt;li&gt;For example, categorize tickets as &lt;strong&gt;trivial&lt;/strong&gt;, &lt;strong&gt;easy&lt;/strong&gt;, &lt;strong&gt;normal&lt;/strong&gt;, &lt;strong&gt;hard&lt;/strong&gt;, &lt;strong&gt;don't know&lt;/strong&gt;, etc.&lt;/li&gt;
&lt;li&gt;This could provide valuable information for prioritization.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

</description>
      <category>management</category>
      <category>softwaredevelopment</category>
      <category>discuss</category>
      <category>productivity</category>
    </item>
    <item>
      <title>The Rewrite Trap: A Programmer's Survival Guide</title>
      <dc:creator>Josephat Macharia</dc:creator>
      <pubDate>Sun, 22 Dec 2024 16:47:25 +0000</pubDate>
      <link>https://dev.to/mashaa/the-rewrite-trap-a-programmers-survival-guide-2hl3</link>
      <guid>https://dev.to/mashaa/the-rewrite-trap-a-programmers-survival-guide-2hl3</guid>
      <description>&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%2Fv725jlr2bxeu5hq6xtv5.jpeg" 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%2Fv725jlr2bxeu5hq6xtv5.jpeg" alt=" " width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the world of software development, there's a constant temptation that lurks in the shadows of every legacy codebase: the urge to throw everything away and start fresh.&lt;/p&gt;

&lt;p&gt;It's a siren song that has led many projects to their doom, yet it continues to seduce developers across the industry.&lt;/p&gt;

&lt;p&gt;Let's dive deep into the art of code refactoring and explore why the "rewrite everything" approach is often a costly mistake.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Temptation of the Blank Slate&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There’s a unique allure in starting fresh.&lt;br&gt;
Like an architect yearning to demolish and rebuild, programmers often see existing codebases as messy, convoluted, or just plain ugly.&lt;/p&gt;

&lt;p&gt;It’s natural to feel this way.&lt;br&gt;
Reading someone else’s code (or even your own, months later) is harder than writing new code from scratch. But this impulse to bulldoze can be catastrophic.&lt;/p&gt;

&lt;p&gt;Here’s the truth: existing code is more than just text on a screen. It’s the culmination of countless hours of debugging, testing and adapting to real-world problems.&lt;/p&gt;

&lt;p&gt;Each line however inelegant—represents accumulated knowledge.&lt;/p&gt;

&lt;p&gt;Tossing it aside without due diligence is like throwing away a treasure map because you dislike the handwriting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Complete Rewrites Are Dangerous&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Rewriting an entire codebase might seem like a clean solution, but history shows it’s often a grave mistake.&lt;/p&gt;

&lt;p&gt;Consider these cautionary tales:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Netscape's decision to rewrite their browser from scratch led to a three-year gap between versions 4.0 and 6.0, giving Internet Explorer the opportunity to dominate the browser market.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Borland's attempt to rewrite Quattro Pro ended in failure.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Even Microsoft, with all its resources, had to abandon a complete rewrite of Word.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rewriting not only wastes time and money but also introduces unknown bugs while losing the hard-earned stability of the original code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Refactor, Don’t Rewrite&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So, what’s the alternative? Incremental refactoring. This approach allows you to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Preserve Battle-Tested Code:&lt;/strong&gt; Even ugly code has survived production, been reviewed and patched over time. Respect the lessons it holds.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Make Incremental Changes:&lt;/strong&gt; Small, focused updates make it easier to track the impact of each change. It’s far less intimidating to fix two failing tests than to face a hundred.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Leverage Existing Tests:&lt;/strong&gt; Never discard old tests without understanding their purpose. These tests may uncover subtle issues you might overlook.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The Right Way to Approach Refactoring&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before refactoring, ask yourself: Why am I doing this?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Performance Issues:&lt;/strong&gt; Profile the code first. Identify bottlenecks and optimize strategically.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Difficulty Adding Features:&lt;/strong&gt; If tight coupling or spaghetti code is the issue, focus on encapsulation and better interfaces.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Code Maintainability:&lt;/strong&gt; Start small, rename variables, clean up formatting and clarify comments. Gradual improvements add up.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Practical Tips for Refactoring Success&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Understand the Existing System:&lt;/strong&gt; Take stock of the codebase and tests. Identify strengths to preserve and weaknesses to address.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Refactor in Small Steps:&lt;/strong&gt; Focus on one area at a time. Verify changes by running tests after each iteration.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Avoid Personal Bias:&lt;/strong&gt; Restructure for functionality, not style. If it isn’t broken, don’t fix it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Beware of New Technology:&lt;/strong&gt; Don’t refactor just to use the latest framework or language. Unless there’s a clear, measurable benefit, it’s not worth the risk.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Respect the Past:&lt;/strong&gt; Every “ugly” workaround or patch likely exists for a reason. Investigate before replacing it.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Conclusion: Progress Without Pain&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Refactoring isn’t just about writing better code, it’s about making smarter decisions.&lt;/p&gt;

&lt;p&gt;By respecting the lessons embedded in your existing system and taking a methodical incremental approach, you’ll avoid the pitfalls of hubris and set yourself up for long-term success.&lt;/p&gt;

&lt;p&gt;Remember, code doesn’t need to be perfect, it just needs to work well for the people who use and maintain it.&lt;/p&gt;

&lt;p&gt;So next time you’re tempted to rewrite everything, stop and think: What’s the real problem I’m solving? The answer might save you countless hours—and a lot of headaches.&lt;/p&gt;

</description>
      <category>refactoring</category>
      <category>cleancode</category>
      <category>softwareengineering</category>
      <category>programming</category>
    </item>
    <item>
      <title>Introduction To RabbitMQ</title>
      <dc:creator>Josephat Macharia</dc:creator>
      <pubDate>Sun, 31 Oct 2021 14:25:26 +0000</pubDate>
      <link>https://dev.to/mashaa/introduction-to-rabbitmq-49n8</link>
      <guid>https://dev.to/mashaa/introduction-to-rabbitmq-49n8</guid>
      <description>&lt;h3&gt;
  
  
  What is RabbitMQ
&lt;/h3&gt;

&lt;p&gt;RabbitMQ is a message broker that accepts and forwards messages. &lt;br&gt;
It accepts messages from &lt;em&gt;producers&lt;/em&gt;, and delivers them to &lt;em&gt;consumers&lt;/em&gt;. It acts like a &lt;strong&gt;middleman&lt;/strong&gt; which can be used to reduce loads and delivery times taken by web application servers.&lt;/p&gt;

&lt;h4&gt;
  
  
  Message flow in RabbitMQ
&lt;/h4&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%2Fi%2F9n502l4r95tmczfgga4m.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%2Fi%2F9n502l4r95tmczfgga4m.png" alt="Alt Text" width="651" height="321"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The producer publishes a message to an exchange. When creating an exchange, the type must be specified.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The exchange receives the message and is now responsible for routing the message. The exchange takes different message attributes into account, such as the routing key, depending on the exchange type.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Bindings must be created from the exchange to queues. In this case, there are two bindings to two different queues from the exchange. The exchange routes the message into the queues depending on message attributes.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;4.The messages stay in the queue until they are handled by a consumer&lt;/p&gt;

&lt;p&gt;5.The consumer handles the message&lt;/p&gt;

&lt;h3&gt;
  
  
  Types of exchanges in RabbitMQ:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Fanout&lt;br&gt;
This exchange basically sends a copy of messages to all the queues it knows. (in the dashboard every queue should be bind ed to one exchange)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Direct&lt;br&gt;
A direct exchange delivers messages to queues based on a message routing key. The routing key is a message attribute added to the message header by the producer. Think of the routing key as an “address” that the exchange is using to decide how to route the message.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Topic&lt;br&gt;
Topic exchanges route messages to one or many queues based on matching between a message routing key and the pattern that was used to bind a queue to an exchange.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;4.Headers&lt;br&gt;
A header exchange is an exchange that route messages to queues based on message header values instead of routing key. If a match is found, it routes the message to the queue whose binding value is matched and if a match is not found, it ignored the message.&lt;/p&gt;

&lt;h3&gt;
  
  
  RabbitMQ Terminologies and Concepts
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Producer&lt;/strong&gt;: A producer is a user application that sends messages to the consumers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consumer&lt;/strong&gt;: A consumer is a user application that receives messages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Message&lt;/strong&gt;: It’s a simple serialized object from the producer!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exchange&lt;/strong&gt;: An exchange is a very simple thing. On one side it receives messages from producers and the other side pushes them to queues. This is a place that messages come first and its main purpose is to decide which consumer application, the message should go!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Queue&lt;/strong&gt;:&lt;br&gt;
A queue is a buffer that stores messages basically, the exchange routes the message to queues, then the consumer clients receive messages from the queue. We have queues as many as the consumer’s applications&lt;br&gt;
The producers send the message to the exchange first, the exchange decides which queues, the message should go. &lt;/p&gt;

&lt;h3&gt;
  
  
  When to Use RabbitMQ
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;For long-running tasks, when you need to run reliable background jobs. e.g such as file scaning, image scaling or PDF&lt;/li&gt;
&lt;li&gt;communication and integration within, and between applications, i.e as middleman between microservices; where a system simply needs to notify another part of the system to start to work on a task, like ordering handling in a webshop (order placed, update order status, send order, payment, etc.)&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>rabbitmq</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
    <item>
      <title>17 days of code complete Day 2 - Chapter 3 and 4</title>
      <dc:creator>Josephat Macharia</dc:creator>
      <pubDate>Fri, 29 Oct 2021 06:18:08 +0000</pubDate>
      <link>https://dev.to/mashaa/17-days-of-code-complete-day-2-chapter-3-and-4-eo8</link>
      <guid>https://dev.to/mashaa/17-days-of-code-complete-day-2-chapter-3-and-4-eo8</guid>
      <description>&lt;p&gt;On day 2 we will look into chapter 3 and 4.&lt;br&gt;
For more in depth notes of the book, please &lt;a href="https://www.amazon.com/Code-Complete-Practical-Handbook-Construction/dp/0735619670" rel="noopener noreferrer"&gt;purchase the book!&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Chapter 3 Measure Twice, Cut Once Upstream Prerequisites
&lt;/h2&gt;

&lt;p&gt;This chapter describes the work that must be done to prepare for software construc-&lt;br&gt;
tion. As with building construction, much of the success or failure of the project has&lt;br&gt;
already been determined before construction begins. If the foundation hasn’t been&lt;br&gt;
laid well or the planning is inadequate, the best you can do during construction is to&lt;br&gt;
keep damage to a minimum.&lt;br&gt;
This chapter discusses software equivalent of blueprints&lt;br&gt;
and construction permits. You might not have much control over preparations, so the focus of this chapter is on assessing what you have to work with when&lt;br&gt;
construction begins.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key take away
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;It is good to plan to specify requirements up front.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Essentially, you’ll need to pick the right balance for your project. If it’s something more stable, where the requirements are well understood and unlikely to change, a more sequential approach is appropriate. But if the requirements may change, or the structure isn’t as well understood, then a more iterative approach will be more beneficial.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The overarching goal of preparation is risk reduction&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Most common project risks in software devel-&lt;br&gt;
opment are poor requirements and poor project planning, thus preparation tends to&lt;br&gt;
focus on improving requirements and project plans.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;From a technical point of&lt;br&gt;
view, planning means understanding what you want to build so that you don’t waste&lt;br&gt;
money building the wrong thing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Explicit requirements keep you from guessing&lt;br&gt;
what the user wants.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Explicit requirements also help to avoid arguments. You decide on the scope of the&lt;br&gt;
system before you begin programming. If you have a disagreement with another pro-&lt;br&gt;
grammer about what the program is supposed to do, you can resolve it by looking at&lt;br&gt;
the written requirements.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Software being what it is, iterative approaches are useful much more often than sequential approaches are.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Handling Requirements Changes During Construction
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Use the requirements checklist at the end of the section to assess the quality of your requirements If your requirements aren’t good enough, stop work, back up, and make them right before you proceed.&lt;/li&gt;
&lt;li&gt;Make sure everyone knows the cost of requirements changes, work up a revised schedule and cost estimate so that you can decide whether you want to do it now or later.&lt;/li&gt;
&lt;li&gt;Set up a change-control procedure If your client’s excitement persists, consider
establishing a formal change-control board to review such proposed changes. It’s all
right for customers to change their minds and to realize that they need more capabilities. The problem is their suggesting changes so frequently that you can’t keep up&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Chapter 4 Key Construction Decisions
&lt;/h2&gt;

&lt;p&gt;This chapter focuses on preparations that individual programmers and technical leads are responsible for, directly or indirectly. It discusses the software equivalent of how to select specific tools for your tool belt and how to load your&lt;br&gt;
truck before you head out to the job site.&lt;/p&gt;

&lt;p&gt;Programmers may be similarly influenced by their languages. The words available in a&lt;br&gt;
programming language for expressing your programming thoughts certainly deter-&lt;br&gt;
mine how you express your thoughts and might even determine what thoughts you&lt;br&gt;
can express.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Your programming tools don’t have to&lt;br&gt;
determine how you think about programming.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Programmers who program “in” a language limit their thoughts to constructs that the language&lt;br&gt;
directly supports. If the language tools are primitive, the programmer’s thoughts will also be primitive.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Programmers who program “into” a language first decide what thoughts they want to express, and then they determine how to express those thoughts using the tools provided by their specific language.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Your position on the technology wave determines what approaches will be effective or even possible. Identify where you are on the technology wave, and adjust your plans and expectations accordingly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Establish programming conventions before you begin programming. It’s nearly&lt;br&gt;
impossible to change code to match them later.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Every programming language has strengths and weaknesses. Be aware of the specific strengths and weaknesses of the language you’re using.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  My thoughts
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Its hard for programmers to adopt to the test first approach.&lt;/li&gt;
&lt;li&gt;Ensuring the programmer has stepped through their code in the debugger before they check it in is not easy.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>programming</category>
    </item>
    <item>
      <title>How To Build APIs That Are Resilent From 3rd Party Failures</title>
      <dc:creator>Josephat Macharia</dc:creator>
      <pubDate>Thu, 28 Oct 2021 20:27:52 +0000</pubDate>
      <link>https://dev.to/mashaa/handling-system-failures-during-3rd-party-api-communications-499j</link>
      <guid>https://dev.to/mashaa/handling-system-failures-during-3rd-party-api-communications-499j</guid>
      <description>&lt;p&gt;There are many possible reasons for failures, from an outage of a third-party service (API, DB, etc.) to a hardware failure, to the “classic” software bug (after all, software developers are humans too, aren’t we? 🤔).&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Fault tolerance is a requirement, not a feature.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This post is an a attempt to address handling system failures during 3rd Party API communications.&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%2F7up532noq9qwjsmbpyf3.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%2F7up532noq9qwjsmbpyf3.png" alt="handling system 3rd party api failure" width="491" height="271"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  What are some of the failure points?
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The network connectivity between the application and the third party system might be disrupted causing communication timeouts or lost information.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The third party system has an internal error or machine failure causing the application not to receive a response.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Third party system has not been paid for, e.g late in account payment renewal.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;We have an internal error or machine failure. Depending on the timing of this failure, two things could happen:&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; 1. Inability to send the request.

 2. Inability to receive the response from the third party.
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  How do we respond to a user request when failure occurs?
&lt;/h4&gt;

&lt;p&gt;The diagram below attempts to show a process that would be followed to achieve resilience in API communication failure&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%2F5vo41ag2yjgg9e2a87kg.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%2F5vo41ag2yjgg9e2a87kg.png" alt="handle payment failure photo" width="800" height="565"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In each of the options described above a timeout, internal error or connection error will result in a request not retrieving the optimal response for customers.&lt;/p&gt;

&lt;h4&gt;
  
  
  What are the possible actions to take when retry will not succeed and is unlikely to help?
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Notifications:  when such 3rd party failure occurs, notifications should be sent for quick action such as calling the customer back to manage expectation and give way out that lets them know that you have their back.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Showing descriptive messages that will aid to let the customer know what to do. e.g refresh, retry&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h5&gt;
  
  
  To improve the resilience of the application we should consider the following patterns:
&lt;/h5&gt;

&lt;p&gt;1.Retry&lt;br&gt;
2.Caching&lt;br&gt;
3.Persistence&lt;br&gt;
4.Circuit Breaker&lt;/p&gt;

&lt;h5&gt;
  
  
  Retry
&lt;/h5&gt;

&lt;p&gt;For a &lt;strong&gt;reliable and robust&lt;/strong&gt; retrying solution it should be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Smart - To answer this, we use exponential backoff retrying.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Customizable - Standard way to handle errors, we should control which errors should be retried and which shouldn’t by throwing exceptions for the errors that should be retried and by catching the others.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h5&gt;
  
  
  Caching
&lt;/h5&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;We record each request in the database before sending the information to an external party. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;We should have a status attribute that tracks which part of the process the request is in. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;We update the request with a new status based on the response, normally either created, declined or successful.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h5&gt;
  
  
  Persistence
&lt;/h5&gt;

&lt;p&gt;To get more inspiration Uber manages the reprocessing of their data using kafka topics and dead-letter queues. See &lt;a href="https://eng.uber.com/reliable-reprocessing/" rel="noopener noreferrer"&gt;https://eng.uber.com/reliable-reprocessing/&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Uber’s idea would fit many of our needs, so we can design a solution based on it.&lt;/p&gt;

&lt;p&gt;First, our error handling solution should identify that there is an error. Then, it needs to grab the relevant context of the error (in this example, the handled HTTP request). &lt;br&gt;
After that, it should send that context to some persistence layer, for later re-execution of the service flow.&lt;br&gt;
When a failure occurs in one of the HTTP requests, the request context (request body, query params, etc.) will be sent as a Kafka message to the first retry Kafka topic. &lt;/p&gt;

&lt;p&gt;As for the re-execution part, a polling of the first retry topic is added and if that processing fails too, it will be sent to the second  topic and so forth, until it’s sent to the dlq (“dead-letter queue”) topic, for manual analysis. (The number of retry topics is arbitrary in this example, it can be any number).&lt;br&gt;
Let’s see how the solution I described applies to service A 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%2Fohf3fn2bav58dqvrcg70.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%2Fohf3fn2bav58dqvrcg70.png" alt=" " width="660" height="572"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Summary
&lt;/h4&gt;

&lt;p&gt;If we implement the solutions I’ve described, we can be much more confident now when our system has failures, the system requires fewer manual interventions when it fails, and most importantly, we sleep well at night!&lt;/p&gt;

&lt;p&gt;I hope this post helped you by providing you with some new ideas for handling failures in your applications.&lt;/p&gt;

&lt;p&gt;Feel free to comment below with any questions/thoughts :-)&lt;/p&gt;

</description>
      <category>devops</category>
      <category>programming</category>
    </item>
    <item>
      <title>When To Use NoSQL Document Databases: MongoDB, CouchDB</title>
      <dc:creator>Josephat Macharia</dc:creator>
      <pubDate>Sat, 08 May 2021 10:33:59 +0000</pubDate>
      <link>https://dev.to/mashaa/when-to-use-nosql-document-databases-mongodb-couchdb-2m27</link>
      <guid>https://dev.to/mashaa/when-to-use-nosql-document-databases-mongodb-couchdb-2m27</guid>
      <description>&lt;h2&gt;
  
  
  Why NoSQL Databases Were Invented
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;When traditional databases were introduced
they were able to handle the size by running 
on bigger machines&lt;/li&gt;
&lt;li&gt;NoSQL means that the database does not employ tables, rows and columns for data organization&lt;/li&gt;
&lt;li&gt;Relational databases could horizontally scale which means being able to copy their data to other machines so reads could be distributed.&lt;/li&gt;
&lt;li&gt;There was no easy way to distribute writes&lt;/li&gt;
&lt;li&gt;To fix this problem, leading technological companies introduced new databases referred to as NoSQL.&lt;/li&gt;
&lt;li&gt;Therefore NoSQL databases were designed to scale large datasets horizontally which means &lt;strong&gt;scaling large datasets&lt;/strong&gt; was the primary motivator to create NoSQL databases.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Types Of NoSQL Databases
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Key/value &lt;/li&gt;
&lt;li&gt;Graph databases&lt;/li&gt;
&lt;li&gt;Column-oriented&lt;/li&gt;
&lt;li&gt;Document-oriented&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Industry examples of Document Databases
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;MongoDB&lt;/li&gt;
&lt;li&gt;CouchDB&lt;/li&gt;
&lt;li&gt;Cosmos DB&lt;/li&gt;
&lt;li&gt;CrateDB&lt;/li&gt;
&lt;li&gt;Couchbase Server &lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Is A Document 
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A way to organize and store data as a set of field value pairs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Features Of Document databases
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Relationships are obvious using embedded arrays and documents.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The biggest benefit to developers of using a document-oriented database is that developers use structure like maps, hashes, dictionaries, and JSON objects.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The document model use a similar structure resulting in the representation that is easier and more natural, as it is the same in the code and the application.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;This means there is no complex mapping between your applications data object and the database.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Today, we know that modern data sets can grow to be huge, that data is polymorphic by nature, and that our modern applications have to be resilient and always up.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;NoSQL databases, and in particular a document-oriented database like MongoDB, are a more natural way to work with modern data.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Use Document Databases
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Applications that take records that are fairly complex and need to access those records in a fairly complex way&lt;/li&gt;
&lt;li&gt;Allows one to have a lot of records with a lot of engineering&lt;/li&gt;
&lt;li&gt;Allows you to have different categories of data and access them the same way&lt;/li&gt;
&lt;li&gt;For rapid application development&lt;/li&gt;
&lt;li&gt;When you need a flexible schema&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When To Use Document Databases
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;When the schema is easily changeable.&lt;/li&gt;
&lt;li&gt;When you want to scale (especially horizontal): they are built for scale since their horizontal partitioning is inbuilt since they expect a lot of scale.&lt;/li&gt;
&lt;li&gt;When you require simplicity e.g fewer tables, fewer relationships&lt;/li&gt;
&lt;li&gt;When you want to aggregate data since they are built for aggregation&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When Not To Use Document Databases
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;When you require to make a lot of updates since consistency is not guaranteed, the data may not be consistent (two nodes might have different data for the same ID)&lt;/li&gt;
&lt;li&gt;They are not read optimized since they read each block and filter out the rows&lt;/li&gt;
&lt;li&gt;They do not have implicit information of relations in data, therefore joins are hard if you have more than one table.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>nosql</category>
      <category>database</category>
    </item>
    <item>
      <title>How Monitoring Works: Prometheus, Grafana and StatsD</title>
      <dc:creator>Josephat Macharia</dc:creator>
      <pubDate>Tue, 27 Apr 2021 07:15:04 +0000</pubDate>
      <link>https://dev.to/mashaa/monitoring-statsd-prometheus-and-grafana-1bpm</link>
      <guid>https://dev.to/mashaa/monitoring-statsd-prometheus-and-grafana-1bpm</guid>
      <description>&lt;h2&gt;
  
  
  What is Monitoring?
&lt;/h2&gt;

&lt;p&gt;The Periodic tracking (for example, daily, weekly, monthly, quarterly, annually) of any activity’s progress by systematically gathering and analyzing data and information&lt;/p&gt;

&lt;h2&gt;
  
  
  Why monitor?
&lt;/h2&gt;

&lt;p&gt;Helps to answer the following:(Imagine or picture this):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What is happenning right now?&lt;/li&gt;
&lt;li&gt;When did things change or what has changed (has it gone up or down did it change when we did this deploy)&lt;/li&gt;
&lt;li&gt;When did it happen&lt;/li&gt;
&lt;li&gt;How often does it happen e.g how many times a second, a day when do people use this feature, how many queries am i sending to the 
database&lt;/li&gt;
&lt;li&gt;How long did it happen.&lt;/li&gt;
&lt;li&gt;Enhance reliability: software bugs can be prevented via:

&lt;ul&gt;
&lt;li&gt;testing&lt;/li&gt;
&lt;li&gt;measuring&lt;/li&gt;
&lt;li&gt;monitoring&lt;/li&gt;
&lt;li&gt;analyzing.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  What is a metric?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The unit that is being monitored for a specific target.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Things to monitor?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;request latency&lt;/li&gt;
&lt;li&gt;request count&lt;/li&gt;
&lt;li&gt;number of exceptions&lt;/li&gt;
&lt;li&gt;cpu utilization&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to Monitor?
&lt;/h2&gt;

&lt;p&gt;we can use traditional methods but why reinvent the wheel use &lt;strong&gt;monitoring tools&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What do monitoring tools offer?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;collection&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Describes how the measurements are taken.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;storage&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;how to get metrics you measured from where you measured into something that will store them long term (usually called time series databases).&lt;/li&gt;
&lt;li&gt;This storage also needs to be capable of fetching that stuff quickly for analysis/graphing/alerting etc.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;graphing &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;visualizing and analyzing metrics collected.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;aggregation windows&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;how long you can keep metrics before you run out of space or the storage becomes slow.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;alerting&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;channels used to notify users when things happen.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  What are some of the common measurement/metric types?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Counter&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%2Fi%2F93jec35uoyt6znb6ic29.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%2Fi%2F93jec35uoyt6znb6ic29.png" alt="Alt Text" width="640" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use this for situations where you want to know “how many times has x happened”.&lt;/li&gt;
&lt;li&gt;for example total number of HTTP requests, or the total number of bytes sent in HTTP&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;Gauge &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%2Fi%2F1ov1dlfdn1m7xqjow1me.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%2Fi%2F1ov1dlfdn1m7xqjow1me.png" alt="Alt Text" width="640" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A representation of a metric that can go both up and down.&lt;/li&gt;
&lt;li&gt; “what is the current value of x now”.&lt;/li&gt;
&lt;li&gt; what is the current value of logged in users now.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Histogram &lt;/p&gt;&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F2ewg0yqus7470i5e6mdw.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%2Fi%2F2ewg0yqus7470i5e6mdw.png" alt="Alt Text" width="640" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;This represents observed metrics sharded into distinct buckets.&lt;/li&gt;
&lt;li&gt;Think of this as a mechanism to track “how long something took” or “how big something was”.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Monitoring patterns for reporting metrics
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Pull model

&lt;ul&gt;
&lt;li&gt;the monitoring system "scrapes" the application at a predefined HTTP endpoint.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Push model, the application sends the data to the monitoring system.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fjlk5096iinpk9fxd8vfu.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%2Fi%2Fjlk5096iinpk9fxd8vfu.png" alt="Alt Text" width="640" height="114"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;An example of a monitoring system working in the pull model is &lt;strong&gt;Prometheus&lt;/strong&gt;. &lt;strong&gt;StatsD&lt;/strong&gt; is an example of a monitoring system where the application pushes the metrics to the system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitoring tools
&lt;/h2&gt;

&lt;p&gt;We will focus on the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;StatsD&lt;/li&gt;
&lt;li&gt;Prometheus&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Whats is StatsD
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;System for instrumenting metrics&lt;/li&gt;
&lt;li&gt;statsd is semi real time and little historical&lt;/li&gt;
&lt;li&gt;a funnel that collects data&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why it was creatd?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Devops guys did not understand the internals of an application apart from its CPU utilization and network IO.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How it works?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;StatsD will accept measurements from all over your network with UDP.&lt;/li&gt;
&lt;li&gt;Aggregates them into 10-seconds chunks.&lt;/li&gt;
&lt;li&gt;Sends them off to somewhere that will store the data e.g Graphite&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What are the components of StatsD?
&lt;/h2&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%2Fi%2Fthpdqihfuqlk5sugoqqj.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%2Fi%2Fthpdqihfuqlk5sugoqqj.png" alt="Alt Text" width="800" height="284"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API Client   --&amp;gt; this integrates with your actual application.
&lt;/li&gt;
&lt;li&gt;UDP Protocol --&amp;gt; API client just wraps over the UDP protocol.&lt;/li&gt;
&lt;li&gt;Daemon --&amp;gt;   this deamon runs on your actual application service.&lt;/li&gt;
&lt;li&gt;Metrics collection: you fit your application with metrics which will send UDP packets to a local daemon
the local daemon will aggregate these packets together and send them in batch to a backend&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What metric types are available?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;counters - how many x times something happened&lt;/li&gt;
&lt;li&gt;timers - it took this long e,g on average&lt;/li&gt;
&lt;li&gt;gauges  - % completion
&lt;/li&gt;
&lt;li&gt;sets - keep track of unique values&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How does it integrate with my app
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Push metrics from your code&lt;/li&gt;
&lt;li&gt;clients are available for many languages e.g ntest statsd&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Which backends it can integrate with
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;graphite&lt;/li&gt;
&lt;li&gt;mysql, influxdb, mongodb&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prometheus  
&lt;/h2&gt;

&lt;p&gt;A monitoring tool that actively scrapes data, stores it, and supports queries, graphs, and alerts, as well as provides endpoints to other API consumers like Grafana or even Graphite itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it exists
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Created to monitor highly dynamic container environments like kubernates and docker swarm etc&lt;/li&gt;
&lt;li&gt;However it can used in a traditional non container infrastructure.&lt;/li&gt;
&lt;li&gt;It has become the mainstream monotoring tool of choice in container and microservice world.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where can i use it?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;When running multiple servers that run containerized applications and there are x(processes)
running on that infrastructure and things are interconnected.&lt;/li&gt;
&lt;li&gt;In complex infrastructure with lots of servers distributed &lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What can go wrong in complex infrastructure?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;One service can crush and cause failure to others.This can be difficult to debug manually.&lt;/li&gt;
&lt;li&gt;Application downtime&lt;/li&gt;
&lt;li&gt;Errors&lt;/li&gt;
&lt;li&gt;Overloaded and running out of resources&lt;/li&gt;
&lt;li&gt;Response latency&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What is the remedy for problems in complex infrastructure
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Have a tool that constantly monitors all the services.&lt;/li&gt;
&lt;li&gt;Alerts the maintainers as soon as one service crashes e.g you know what happened.&lt;/li&gt;
&lt;li&gt;Identify problems before they occur and alert the system admin responsible for that infrastructure to prevent that issue e.g

&lt;ul&gt;
&lt;li&gt;when the application is about to run out of storage space&lt;/li&gt;
&lt;li&gt;when an application becomes too slow, have tool that detects network spikes&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it offers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Automated monitoring and alerting for devops workflow&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Architecture/How it works
&lt;/h2&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%2Fi%2Frck6bqxdumvhn2wvau9z.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%2Fi%2Frck6bqxdumvhn2wvau9z.png" alt="Alt Text" width="800" height="560"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Prometheus Server&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does the actual monitoring work and made up of three components

&lt;ol&gt;
&lt;li&gt;Storage: time series database --&amp;gt; stores all the metrics data e.g current cpu usage, no_of_requests&lt;/li&gt;
&lt;li&gt;Data retrieval worker responsible for getting or pulling those metrics and pushing them to the database&lt;/li&gt;
&lt;li&gt;Server API That accepts queries for that stored data and used to display the data/visualize inside prometheus or other graph tool&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What is the work of prometheus alert Manager
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Responsible for firing alerts via different channels e.g email, slack.&lt;/li&gt;
&lt;li&gt;Determines who receives? how it triggers.&lt;/li&gt;
&lt;li&gt;Reads the rule, if the condition is met then an alert is fired.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where is data stored
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Stores the data on disk also integrates with remote storage.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What are the characteristics
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Designed to be reliable even when other services have an outage.&lt;/li&gt;
&lt;li&gt;Standalone and self containing --&amp;gt; does not depend on network storage&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it Monitors
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A particulat thing e.g a linux server, windows, database server&lt;/li&gt;
&lt;li&gt;Things it monitors are called targets 

&lt;ul&gt;
&lt;li&gt;For linux server it can be, CPU status, memory disk, space usage&lt;/li&gt;
&lt;li&gt;For application server it can be exceptions count, number of request and request duration&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  There are 3 primitive metrics types
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Counter --&amp;gt; How many times x happened&lt;/li&gt;
&lt;li&gt;Gauge  --&amp;gt; what is the current value of x now or what is the current capacity of disk space now&lt;/li&gt;
&lt;li&gt;Histogram/Timer ---&amp;gt; How long or how big&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How it collects metrics
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;It pulls metrics from a http endpoint whose host adress exposes a &lt;strong&gt;/metrics&lt;/strong&gt; endpoint.
for that to work:

&lt;ul&gt;
&lt;li&gt;The target must expose the &lt;strong&gt;/metrics&lt;/strong&gt; endpoint&lt;/li&gt;
&lt;li&gt;Data available at &lt;strong&gt;/metrics&lt;/strong&gt; must be in a format that prometheus understands&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Target Endpoints and exporters
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Some servers are already exposing the /metrics endpoint so no extra work is needed to get metrics from them&lt;/li&gt;
&lt;li&gt;Many services dont have native prometheus endpoint and need an extra component known as an exporter&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Exporter is a script or service that fetches  metrics from ur target and converts them into a format prometheus understands&lt;br&gt;
and exposes these metrics to its own /metrics endpoint&lt;/p&gt;

&lt;h2&gt;
  
  
  How to monitor a linux/windows server
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Download a node exporter&lt;/li&gt;
&lt;li&gt;untar and execute&lt;/li&gt;
&lt;li&gt;converts metrics of the server&lt;/li&gt;
&lt;li&gt;exposes /metrics endpoint&lt;/li&gt;
&lt;li&gt;configure promethues to scrape this endpoint&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to monitor an application
&lt;/h2&gt;

&lt;p&gt;At application level you can monitor the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How long request are taking

&lt;ul&gt;
&lt;li&gt;How many requests
Use client libraries to expose /metrics endpoint&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why care
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;In push mechanism applications/servers push to a centralized platform&lt;/li&gt;
&lt;li&gt;high load of network traffic: when working with many microservices and you have each service pushing their metrics
to the monitoring system it creates a high load of traffic in your infrastructre and your monitoring can be your bottleneck&lt;/li&gt;
&lt;li&gt;must install a daemon on each target to push data to the monitoring server.&lt;/li&gt;
&lt;li&gt;Better detect/insight if service is up and running&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;note: for shortlived gateway it offers a pushgateway&lt;/p&gt;

&lt;h2&gt;
  
  
  How it know what to scape and when?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;All that is confugured in promethues.yml:

&lt;ul&gt;
&lt;li&gt;define what targets to scrape and at what interval.&lt;/li&gt;
&lt;li&gt;It uses a service discovery to discover those endpoints.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  What is it does not do well
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Dificult to scale when you have 100 of servers you might want to have multiple prometheus servers that somewhat
aggregate all these metrics data and configuring that can be dificlut&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Workarounds
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Increase prometheus server capacity.&lt;/li&gt;
&lt;li&gt;Limit number of metrics.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Example
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Graphana
&lt;/h2&gt;

&lt;p&gt;Allows you to query, visualize alert and understand your metrics no matter where they are stored.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Enables you to create, explore and share dashboards with your team.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example:&lt;br&gt;
Explore metrics &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ad-hoc queries are queries that are made interactively, with the purpose of exploring data. An ad-hoc query is commonly followed by another, more specific query&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;1.tns_request_duration_seconds_count&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;is a counter, a type of metric whose value only ever increases. Rather than visualizing the actual value, you can use counters to calculate the rate of change, i.e. how fast the value increases.

&lt;ol&gt;
&lt;li&gt;Add the rate function to your query to visualize the rate of requests per second. Enter the following in the Query editor and then press Shift + Enter.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;rate(tns_request_duration_seconds_count[5m])&lt;/p&gt;

&lt;p&gt;Immediately below the graph there’s an area where each time series is listed with a colored icon next to it. This area is called the legend.&lt;/p&gt;

&lt;p&gt;PromQL lets you group the time series by their labels, using the sum function.&lt;/p&gt;

&lt;p&gt;Add the sum function to your query to group time series by route:&lt;/p&gt;

&lt;p&gt;sum(rate(tns_request_duration_seconds_count[5m])) by(route)&lt;/p&gt;

&lt;h2&gt;
  
  
  Add a logging data source
&lt;/h2&gt;

&lt;p&gt;Grafana supports log data sources, like Loki. Just like for metrics, you first need to add your data source to Grafana.&lt;/p&gt;

&lt;h2&gt;
  
  
  Loki
&lt;/h2&gt;

&lt;p&gt;Grafana Loki is a set of components that can be composed into a fully featured logging stack.&lt;/p&gt;

&lt;p&gt;Grafana supports log data sources, like Loki. Just like for metrics, you first need to add your data source to Grafana.&lt;/p&gt;

&lt;p&gt;Explore logs&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;{filename="/var/log/tns-app.log"}&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Grafana displays all logs within the log file of the sample application. The height of each bar encodes the number of logs that were generated at that time.&lt;/p&gt;

&lt;p&gt;{filename="/var/log/tns-app.log"} |= "error"&lt;/p&gt;

&lt;p&gt;Logs are helpful for understanding what went wrong&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you can correlate logs with metrics from Prometheus to better understand the context of the error.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Build a Dashboard
&lt;/h2&gt;

&lt;p&gt;A dashboard gives you an at-a-glance view of your data and lets you track metrics through different visualizations.&lt;/p&gt;

&lt;p&gt;Dashboards consist of panels, each representing a part of the story you want your dashboard to tell.&lt;/p&gt;

&lt;p&gt;Every panel consists of a query and a visualization. The query defines what data you want to display, whereas the visualization defines how the data is displayed.&lt;br&gt;
Unlike other logging systems, Loki is built around the idea of only indexing metadata about your logs: labels (just like Prometheus labels).&lt;/p&gt;

&lt;h2&gt;
  
  
  Annotate events
&lt;/h2&gt;

&lt;p&gt;When things go bad, it often helps if you understand the context in which the failure occurred. Time of last deploy, system changes, or database migration can offer insight into what might have caused an outage. Annotations allow you to represent such events directly on your graphs.&lt;/p&gt;

&lt;p&gt;query annotations&lt;br&gt;
{filename="/var/log/tns-app.log"} |= "error"&lt;/p&gt;

&lt;h2&gt;
  
  
  Alerts
&lt;/h2&gt;

&lt;p&gt;Alerts allow you to identify problems in your system moments after they occur. By quickly identifying unintended changes in your system, you can minimize disruptions to your services.&lt;/p&gt;

&lt;p&gt;Alerts consists of two parts:&lt;/p&gt;

&lt;p&gt;Notification channel - How the alert is delivered. When the conditions of an alert rule are met, the Grafana notifies the channels configured for that alert.&lt;br&gt;
Alert rules - When the alert is triggered. Alert rules are defined by one or more conditions that are regularly evaluated by Grafana.&lt;/p&gt;

&lt;p&gt;Supported Alert Notification Channels&lt;br&gt;
Email&lt;br&gt;
Slack&lt;br&gt;
Kafka&lt;br&gt;
Google Hangouts Chat&lt;br&gt;
Microsoft Teams&lt;br&gt;
etc.&lt;/p&gt;

&lt;h2&gt;
  
  
  Others to look at
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;TICK stack&lt;/li&gt;
&lt;li&gt;Google cloud Monitoring&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devops</category>
      <category>monitoring</category>
      <category>programming</category>
    </item>
    <item>
      <title>Document Databases e.g MongoDB</title>
      <dc:creator>Josephat Macharia</dc:creator>
      <pubDate>Tue, 27 Oct 2020 14:25:36 +0000</pubDate>
      <link>https://dev.to/mashaa/document-databases-e-g-mongodb-508k</link>
      <guid>https://dev.to/mashaa/document-databases-e-g-mongodb-508k</guid>
      <description>&lt;p&gt;This course focuses on mapping the knowledge of:&lt;/p&gt;

&lt;p&gt;architects designing a database schema&lt;br&gt;
developers accessing the database from an application&lt;br&gt;
from SQL or RDBMS (Relational DataBase Management Systems) to MongoDB&lt;/p&gt;

</description>
    </item>
    <item>
      <title>17 days of code complete Day 1 - Chapter 1 and 2</title>
      <dc:creator>Josephat Macharia</dc:creator>
      <pubDate>Mon, 14 Sep 2020 21:19:04 +0000</pubDate>
      <link>https://dev.to/mashaa/17-days-of-code-complete-day-1-chapter-one-and-two-4740</link>
      <guid>https://dev.to/mashaa/17-days-of-code-complete-day-1-chapter-one-and-two-4740</guid>
      <description>&lt;p&gt;In the coming days i will be giving a summary and my own review and thoughts of &lt;a href="https://www.amazon.com/Code-Complete-Practical-Handbook-Construction/dp/0735619670" rel="noopener noreferrer"&gt;Code Complete&lt;/a&gt; book by Steve McConnell chapter by chapter. I hope you find this book review helpful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chapter 1
&lt;/h2&gt;

&lt;p&gt;What Is Software Construction?&lt;/p&gt;

&lt;p&gt;In common usage, “construction” refers to&lt;br&gt;
the process of building. The construction process might include some aspects of&lt;br&gt;
planning, designing, and checking your work, but mostly “construction” refers to&lt;br&gt;
the hands-on part of creating something&lt;/p&gt;

&lt;h2&gt;
  
  
  Construction activities:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;coding and debugging &lt;/li&gt;
&lt;li&gt;detailed design &lt;/li&gt;
&lt;li&gt;unit testing&lt;/li&gt;
&lt;li&gt;integration testing&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tasks involved in construction activities:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Verifying that the groundwork has been laid so that construction can proceed
successfully.&lt;/li&gt;
&lt;li&gt;Determining how your code will be tested.&lt;/li&gt;
&lt;li&gt;Designing and writing classes and routines.&lt;/li&gt;
&lt;li&gt;Creating and naming variables and named constants.&lt;/li&gt;
&lt;li&gt;Selecting control structures and organizing blocks of statements.&lt;/li&gt;
&lt;li&gt;Unit testing, integration testing, and debugging your own code.&lt;/li&gt;
&lt;li&gt;Reviewing other team members’ low-level designs and code and having
them review yours.&lt;/li&gt;
&lt;li&gt;Polishing code by carefully formatting and commenting it.&lt;/li&gt;
&lt;li&gt;Integrating software components that were created separately.&lt;/li&gt;
&lt;li&gt;Tuning code to make it smaller and faster.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Nonconstruction activities:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Management.&lt;/li&gt;
&lt;li&gt;Requirements development.&lt;/li&gt;
&lt;li&gt;Software architecture.&lt;/li&gt;
&lt;li&gt;User-interface design.&lt;/li&gt;
&lt;li&gt;System testing and maintenance&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Is Software Construction Important? 
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Putting construction in its context with other activities helps&lt;br&gt;
keep the focus on the right tasks during construction and appropriately&lt;br&gt;
emphasizes important nonconstruction activities.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Construction is a large part of software development.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Construction is the central activity in software development.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;individual programmer’s productivity can improve enormously. &lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  My thoughts:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;When tuning code to make it smaller and faster one should be pragmatic about choosing algorithms, the fastest is not always the best for the job&lt;/li&gt;
&lt;li&gt;Design to test, build the tests before you implement the code.build testability into the software from the very beginning.&lt;/li&gt;
&lt;li&gt;Continuous integration and delivery should be part of the construction process to prevent the &lt;em&gt;integration hell&lt;/em&gt;, although integrating code frequently does not by itself offer any guarantees about the quality of the new code or functionality.&lt;/li&gt;
&lt;li&gt;Although not mandatory including acceptance testing to be part of the construction process minimizes the amount of bugs that could be realized. &lt;/li&gt;
&lt;li&gt;Well written code should be self documenting and does not need comments. &lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Chapter 2: Metaphors for a Richer Understanding of Software Development
&lt;/h2&gt;

&lt;p&gt;A software metaphor is more like a searchlight than a roadmap. It doesn’t tell&lt;br&gt;
you where to find the answer; it tells you how to look for it. A metaphor serves&lt;br&gt;
more as a heuristic than it does as an algorithm.&lt;/p&gt;

&lt;p&gt;An &lt;em&gt;algorithm&lt;/em&gt; is predictable, deterministic, and not subject to chance&lt;/p&gt;

&lt;p&gt;A &lt;em&gt;heuristic&lt;/em&gt; is a technique that helps you look for an answer. Its results are&lt;br&gt;
subject to chance because a heuristic tells you only how to look, not what to find.&lt;br&gt;
It doesn’t tell you how to get directly from point A to point B; it might not even&lt;br&gt;
know where point A and point B are. In effect, a heuristic is an algorithm in a&lt;br&gt;
clown suit. It’s less predictable, it’s more fun, and it comes without a 30-day&lt;br&gt;
money-back guarantee&lt;/p&gt;

&lt;p&gt;The main difference between the two is the level of indirection from the solution.&lt;br&gt;
An algorithm gives you the instructions directly. A heuristic tells you how to discover the instructions for&lt;br&gt;
yourself, or at least where to look for them.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Use Software Metaphors
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Use them to give you insight into your programming problems and processes.&lt;/li&gt;
&lt;li&gt;Use them to help you think about your Programming activities and to help you imagine better ways of doing things.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Importance of Metaphors
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Metaphors have the virtue of an expected behavior that is understood by all.&lt;/li&gt;
&lt;li&gt;Unnecessary communication and misunderstandings are reduced.&lt;/li&gt;
&lt;li&gt;Learning and education are quicker.&lt;/li&gt;
&lt;li&gt;In effect, metaphors are a way of internalizing and abstracting concepts allowing one’s
thinking to be on a higher plane and low-levelmistakes to be avoided.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common Software Metaphors
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Software Penmanship: Writing Code&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“writing code.” The writing metaphor suggests that developing a
program is like writing a casual letter—you sit down with pen, ink, and paper
and write it from start to finish.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Software Farming: Growing a System&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Creating software as something like planting seeds and growing
crops. You design a piece, code a piece, test a piece, and add it to the system a
little bit at a time. By taking small steps, you minimize the trouble you can get
into at any one time.&lt;/li&gt;
&lt;li&gt;If you buy into the farming metaphor, you might find yourself talking
about fertilizing the system plan, thinning the detailed design, increasing code
yields through effective land management, and harvesting the code itself. &lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Software Oyster Farming: System Accretion&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Learn how to add to your software systems a
small amount at a time. Other words closely related to 
accretion are
“incremental,” “iterative,” “adaptive,” and “evolutionary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;4.Software Construction: Building Software&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;If you’re building a first-class software product, you&lt;br&gt;
might build your own scientific functions for better speed or accuracy. You&lt;br&gt;
might build your own container classes, user interface &lt;br&gt;
classes and database&lt;br&gt;
classes to give your system a seamless, perfectly consistent &lt;br&gt;
look and feel.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Both building construction and software construction both benefit from&lt;br&gt;
appropriate levels of planning. If you build software in the wrong order, it’s hard&lt;br&gt;
to code, hard to test, and hard to debug. It can take longer to complete, or the&lt;br&gt;
project can fall apart because everyone’s work is too complex and therefore too&lt;br&gt;
confusing when it’s all combined.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The more experienced you have with the&lt;br&gt;
kind of software you’re building, the more details you can take for granted. You&lt;br&gt;
just want to be sure that you plan enough so that lack of planning doesn’t create&lt;br&gt;
major problems later.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In software you might generally use flexible,&lt;br&gt;
lightweight development approaches, but sometimes rigid, heavyweight&lt;br&gt;
approaches are required to achieve safety goals and other goals.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Making structural changes in a program costs more than adding or deleting peripheral features.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;5.Toolbox Metaphor&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A good craftsman knows the&lt;br&gt;
right tool for the job and knows how to use it correctly. Programmers do too.&lt;br&gt;
The more you learn about programming, the more you fill your mental toolbox&lt;br&gt;
with analytical tools and the knowledge of when to use them and how to use&lt;br&gt;
them correctly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The toolbox metaphor helps to keep all the methods, techniques, and tips in&lt;br&gt;
perspective—ready for use when appropriate.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To extend the “building a house metaphor” construction workers do not use a single tool — the hammer — to build a house. Instead, they have a complete toolbox full of different implements for the varied tasks involved in construction. Likewise, in software engineering, or data science, the person who only knows one method will not get very far.You should be open to learning from others, respond constructively to feedback, and fill your toolbox with the appropriate techniques.&lt;/p&gt;

</description>
      <category>programming</category>
    </item>
  </channel>
</rss>
