<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet type="text/xsl" href="/feed.xsl"?><feed xmlns="http://www.w3.org/2005/Atom">
    <title>CSlant</title>
    <subtitle></subtitle>
    <author><name>CSlant</name></author>
    <link href="https://cslant.com/feed.atom" rel="self" />
    <link href="https://cslant.com" />
    <id>https://cslant.com/feed.atom</id>
    <updated>2026-09-03T11:49:05+07:00</updated>
        <entry>
        <title>Exponential Backoff with Jitter: Retrying Failed Requests the Right Way</title>
        <link href="https://cslant.com/tips/exponential-backoff-with-jitter-retrying-failed-requests-the-right-way" />
        <id>https://cslant.com/tips/exponential-backoff-with-jitter-retrying-failed-requests-the-right-way</id>
        <updated>2026-09-03T11:49:05+07:00</updated>
                        <category term="DevOps" />
                        <summary>Why naive exponential backoff causes retry storms, and how adding random jitter smooths out the load, with a full-jitter retry implementation in PHP and Python.</summary>
                        <content type="html"><![CDATA[<p>Retrying a failed request seems simple: wait a bit, try again. But retry the same fixed delay across thousands of clients and you get a thundering herd — every client wakes up and hammers the server at the exact same moment, often taking down whatever was just starting to recover.</p>
<p>Exponential backoff spaces retries further apart each time (<code>200ms, 400ms, 800ms...</code>), but on its own it still leaves every client synchronized to the same schedule. Jitter fixes that by randomizing the delay, so retries spread out instead of arriving in lockstep.</p>
<h2>The Retry Timeline</h2>
<div class="page-mermaid"><div class="mermaid">graph LR
A[Attempt 1] --&gt;|delay ~200ms| B[Attempt 2]
B --&gt;|delay ~400ms plus jitter| C[Attempt 3]
C --&gt;|delay ~800ms plus jitter| D[Attempt 4]
D --&gt;|delay ~1600ms plus jitter, capped| E[Give up or succeed]</div></div>
<p>Each hop roughly doubles the base delay, capped at some maximum, with a random component layered on top so no two clients retry at exactly the same instant.</p>
<h2>Fixed vs Exponential vs Jittered</h2>
<p>A fixed delay (always wait 1s) is simple but synchronizes every client that failed at the same time. Plain exponential backoff (<code>200ms, 400ms, 800ms, 1600ms</code>) spreads attempts further apart over time, but clients that failed together are still retrying together at each step. Adding jitter — picking a random delay instead of the fixed exponential value — breaks that synchronization entirely.</p>
<h2>Full Jitter</h2>
<p>The <code>full jitter</code> strategy (used internally by AWS's own SDKs) computes the exponential delay as a ceiling, then picks a uniformly random value between zero and that ceiling:</p>
<pre><code>delay = random_between(0, min(cap, base * 2^attempt))
</code></pre>
<p>This is simpler than "equal jitter" or "decorrelated jitter" variants and performs well in practice: it spreads retries across the full window instead of just adding a small amount of noise around the exponential value.</p>
<h2>Implementation</h2>
<div class="page-codeblock" x-data="{ active: 0 }"><div class="page-codeblock__bar"><div class="flex items-center gap-1 overflow-x-auto"><button type="button" @click="active = 0" class="tip-tab" :class="active === 0 ? 'tip-tab--active' : ''">PHP</button><button type="button" @click="active = 1" class="tip-tab" :class="active === 1 ? 'tip-tab--active' : ''">PYTHON</button></div></div><div x-show="active === 0" x-cloak=""><pre><code class="language-php">function calculateBackoff(int $attempt, int $baseDelayMs = 200, int $maxDelayMs = 30000): int
{
    $exponential = $baseDelayMs * (2 ** $attempt);
    $capped = min($exponential, $maxDelayMs);

    return random_int(0, $capped);
}

function retryWithBackoff(callable $operation, int $maxAttempts = 5, int $baseDelayMs = 200, int $maxDelayMs = 30000)
{
    $attempt = 0;

    while (true) {
        try {
            return $operation();
        } catch (\Throwable $e) {
            $attempt++;

            if ($attempt &gt;= $maxAttempts) {
                throw $e;
            }

            $delayMs = calculateBackoff($attempt, $baseDelayMs, $maxDelayMs);
            usleep($delayMs * 1000);
        }
    }
}</code></pre></div><div x-show="active === 1" x-cloak=""><pre><code class="language-python">import random
import time
from typing import Callable, TypeVar

T = TypeVar("T")


def calculate_backoff(attempt: int, base_delay_ms: int = 200, max_delay_ms: int = 30000) -&gt; int:
    exponential = base_delay_ms * (2 ** attempt)
    capped = min(exponential, max_delay_ms)

    return random.randint(0, capped)


def retry_with_backoff(
    operation: Callable[[], T],
    max_attempts: int = 5,
    base_delay_ms: int = 200,
    max_delay_ms: int = 30000,
) -&gt; T:
    attempt = 0

    while True:
        try:
            return operation()
        except Exception:
            attempt += 1

            if attempt &gt;= max_attempts:
                raise

            delay_ms = calculate_backoff(attempt, base_delay_ms, max_delay_ms)
            time.sleep(delay_ms / 1000)</code></pre></div></div>
<h2>Choosing the Parameters</h2>
<p><code>baseDelayMs</code> should roughly match how fast the downstream service actually recovers from a blip — too low and you're retrying into a still-failing service, too high and users wait needlessly on transient errors. <code>maxDelayMs</code> caps the worst case so a client doesn't end up waiting minutes between attempts. <code>maxAttempts</code> should be low enough that a genuinely broken dependency fails fast instead of hanging the caller for a long chain of doubling delays.</p>
<h2>Common Pitfalls</h2>
<p>Retrying without any cap on the delay means a client can end up waiting minutes for an attempt that will just fail again. Retrying without jitter defeats the entire purpose — synchronized clients stay synchronized no matter how far apart the delays get. Retrying on every kind of error, including 4xx client errors that will never succeed, wastes the retry budget on failures that a delay can't fix. And retrying indefinitely with no <code>maxAttempts</code> turns a transient blip into a request that never gives up and never surfaces an error to the caller.</p>
<h2>When to Reach for This</h2>
<p>Any call to a dependency that can fail transiently and recover on its own: HTTP calls to another service, database connections during a failover, queue consumers hitting a rate-limited API. If failures are rarely transient — a malformed request, a missing resource — backoff and retries just delay an error that was never going to succeed.</p>]]></content>
            </entry>
        <entry>
        <title>Consistent Hashing: Distributing Load Without a Full Reshuffle</title>
        <link href="https://cslant.com/tips/consistent-hashing-distributing-load-without-a-full-reshuffle" />
        <id>https://cslant.com/tips/consistent-hashing-distributing-load-without-a-full-reshuffle</id>
        <updated>2026-08-28T23:33:56+07:00</updated>
                        <category term="DevOps" />
                        <summary>How consistent hashing distributes keys across nodes so adding or removing a node only reshuffles a small fraction of the keyspace, with a virtual-node implementation in PHP and Python.</summary>
                        <content type="html"><![CDATA[<p>Naive hashing (<code>hash(key) % N</code>) works fine until you add or remove a node — then N changes and almost every key maps to a different server. For a distributed cache or shard, that means a near-total cache miss storm or a mass data migration, right when you can least afford it.</p>
<p>Consistent hashing fixes this by mapping both nodes and keys onto the same ring, so a topology change only remaps the keys between the changed node and its neighbor.</p>
<h2>The Ring</h2>
<div class="page-mermaid"><div class="mermaid">graph LR
    subgraph Ring["Hash Ring (0 to 2^32-1)"]
        NA((Node A))
        NB((Node B))
        NC((Node C))
        K1[key1] -.-&gt; NB
        K2[key2] -.-&gt; NC
        K3[key3] -.-&gt; NA
    end</div></div>
<p>Each node is hashed onto the ring (often multiple times — see "Virtual Nodes" below). To find which node owns a key, hash the key and walk clockwise until you hit the first node.</p>
<h2>Adding or Removing a Node</h2>
<p>When node B leaves, only the keys that were mapped to B move — to B's clockwise neighbor. Every other key stays exactly where it was. That's the whole point: roughly <code>1/N</code> of the keyspace moves per topology change, not the whole keyspace.</p>
<h2>Virtual Nodes (Replicas)</h2>
<p>Hashing raw node names onto the ring directly creates hot spots — some nodes end up owning much bigger arcs than others. The fix: hash each node multiple times, e.g. <code>node-A#0</code>, <code>node-A#1</code>, ..., <code>node-A#149</code> (150 virtual nodes is a common default). More virtual nodes means smoother distribution, at the cost of a bigger in-memory ring.</p>
<h2>Implementation</h2>
<div class="page-codeblock" x-data="{ active: 0 }"><div class="page-codeblock__bar"><div class="flex items-center gap-1 overflow-x-auto"><button type="button" @click="active = 0" class="tip-tab" :class="active === 0 ? 'tip-tab--active' : ''">PHP</button><button type="button" @click="active = 1" class="tip-tab" :class="active === 1 ? 'tip-tab--active' : ''">PYTHON</button></div></div><div x-show="active === 0" x-cloak=""><pre><code class="language-php">class ConsistentHash
{
    private array $ring = [];
    private array $sortedKeys = [];
    private int $replicas;

    public function __construct(int $replicas = 150)
    {
        $this-&gt;replicas = $replicas;
    }

    public function addNode(string $node): void
    {
        for ($i = 0; $i &lt; $this-&gt;replicas; $i++) {
            $this-&gt;ring[$this-&gt;hash("$node#$i")] = $node;
        }
        $this-&gt;sortedKeys = $this-&gt;sortedRingKeys();
    }

    public function getNode(string $key): ?string
    {
        if (empty($this-&gt;sortedKeys)) {
            return null;
        }

        $hash = $this-&gt;hash($key);
        foreach ($this-&gt;sortedKeys as $ringKey) {
            if ($ringKey &gt;= $hash) {
                return $this-&gt;ring[$ringKey];
            }
        }

        return $this-&gt;ring[$this-&gt;sortedKeys[0]];
    }

    private function sortedRingKeys(): array
    {
        $keys = array_keys($this-&gt;ring);
        sort($keys);
        return $keys;
    }

    private function hash(string $value): int
    {
        return crc32($value);
    }
}</code></pre></div><div x-show="active === 1" x-cloak=""><pre><code class="language-python">import bisect
import zlib


class ConsistentHash:
    def __init__(self, replicas: int = 150):
        self.replicas = replicas
        self.ring: dict[int, str] = {}
        self.sorted_keys: list[int] = []

    def add_node(self, node: str) -&gt; None:
        for i in range(self.replicas):
            self.ring[self._hash(f"{node}#{i}")] = node
        self.sorted_keys = sorted(self.ring.keys())

    def get_node(self, key: str) -&gt; str | None:
        if not self.sorted_keys:
            return None

        h = self._hash(key)
        index = bisect.bisect_left(self.sorted_keys, h)
        if index == len(self.sorted_keys):
            index = 0

        return self.ring[self.sorted_keys[index]]

    @staticmethod
    def _hash(value: str) -&gt; int:
        return zlib.crc32(value.encode())</code></pre></div></div>
<h2>Common Pitfalls</h2>
<p>Too few virtual nodes leads to uneven load, with hot spots on a handful of physical nodes. Using a weak or poorly-distributed hash function makes the same problem worse — prefer something like CRC32 or xxHash over a naive sum-of-bytes hash. Forgetting to remove all of a node's virtual nodes on removal leaves ghost entries that route traffic to a dead server. And rehashing the entire keyspace on every topology change defeats the whole purpose — only the ring should change, not every key's mapping.</p>
<h2>When to Reach for This</h2>
<p>Distributed caches doing client-side sharding, CDN edge selection, distributed hash tables, sharded databases, and load balancers that need session affinity without a central coordinator. If your cluster size is fixed and rarely changes, plain modulo hashing is simpler and perfectly fine.</p>]]></content>
            </entry>
        <entry>
        <title>LRU Cache: Design an O(1) Cache with a Doubly Linked List and Hash Map</title>
        <link href="https://cslant.com/tips/lru-cache-design-an-o1-cache-with-a-doubly-linked-list-and-hash-map" />
        <id>https://cslant.com/tips/lru-cache-design-an-o1-cache-with-a-doubly-linked-list-and-hash-map</id>
        <updated>2026-08-28T10:00:00+07:00</updated>
                        <category term="PHP" />
                        <summary>How to design an LRU cache that supports get and put in O(1) time using a hash map paired with a doubly linked list, with a working PHP implementation and a hands-on coding challenge.</summary>
                        <content type="html"><![CDATA[<p>An LRU (Least Recently Used) cache evicts the item that hasn't been touched in the longest time once it's full. It's a classic interview question, but it's also exactly what backs real systems: database query caches, CDN edge caches, browser tab memory management, connection pools.</p>
<p>The naive approach — an array you scan for the oldest entry — is O(n) per operation. The trick to O(1) is combining two structures: a hash map for O(1) lookup, and a doubly linked list for O(1) reordering.</p>
<h2>The Structure</h2>
<div class="page-mermaid"><div class="mermaid">graph LR
    subgraph "Hash Map (key -&gt; node pointer)"
        M1["'a' -&gt; Node A"]
        M2["'b' -&gt; Node B"]
        M3["'c' -&gt; Node C"]
    end

    subgraph "Doubly Linked List (most to least recent)"
        HEAD((head)) &lt;--&gt; A["Node A key=a"]
        A &lt;--&gt; B["Node B key=b"]
        B &lt;--&gt; C["Node C key=c"]
        C &lt;--&gt; TAIL((tail))
    end

    M1 -.-&gt; A
    M2 -.-&gt; B
    M3 -.-&gt; C</div></div>
<p>Every node lives in the linked list AND has a pointer stored in the hash map. <code>head</code> is a sentinel pointing to the most recently used node; <code>tail</code> is a sentinel pointing to the least recently used one, which is the eviction candidate.</p>
<h2>The Two Operations</h2>
<p><strong>get(key):</strong> hash map lookup finds the node in O(1). If found, unlink it from its current position and relink it right after <code>head</code> (it's now the most recently used), then return its value. If not found, return null/miss.</p>
<p><strong>put(key, value):</strong> if the key already exists, update its value and move it to the front, same as get. If it's new and capacity is full, remove the node just before <code>tail</code> (the least recently used) from both the list and the hash map, then insert the new node at the front.</p>
<p>Both operations only ever touch a constant number of pointers — no scanning required.</p>
<h2>PHP Implementation</h2>
<pre><code class="language-php">class Node
{
    public function __construct(
        public string $key,
        public mixed $value,
        public ?Node $prev = null,
        public ?Node $next = null,
    ) {}
}

class LRUCache
{
    private array $map = [];
    private Node $head;
    private Node $tail;
    private int $capacity;

    public function __construct(int $capacity)
    {
        $this-&gt;capacity = $capacity;
        $this-&gt;head = new Node('', null);
        $this-&gt;tail = new Node('', null);
        $this-&gt;head-&gt;next = $this-&gt;tail;
        $this-&gt;tail-&gt;prev = $this-&gt;head;
    }

    public function get(string $key): mixed
    {
        if (!isset($this-&gt;map[$key])) {
            return null;
        }

        $node = $this-&gt;map[$key];
        $this-&gt;detach($node);
        $this-&gt;attachToFront($node);

        return $node-&gt;value;
    }

    public function put(string $key, mixed $value): void
    {
        if (isset($this-&gt;map[$key])) {
            $node = $this-&gt;map[$key];
            $node-&gt;value = $value;
            $this-&gt;detach($node);
            $this-&gt;attachToFront($node);
            return;
        }

        if (count($this-&gt;map) &gt;= $this-&gt;capacity) {
            $lru = $this-&gt;tail-&gt;prev;
            $this-&gt;detach($lru);
            unset($this-&gt;map[$lru-&gt;key]);
        }

        $node = new Node($key, $value);
        $this-&gt;map[$key] = $node;
        $this-&gt;attachToFront($node);
    }

    private function detach(Node $node): void
    {
        $node-&gt;prev-&gt;next = $node-&gt;next;
        $node-&gt;next-&gt;prev = $node-&gt;prev;
    }

    private function attachToFront(Node $node): void
    {
        $node-&gt;next = $this-&gt;head-&gt;next;
        $node-&gt;prev = $this-&gt;head;
        $this-&gt;head-&gt;next-&gt;prev = $node;
        $this-&gt;head-&gt;next = $node;
    }
}
</code></pre>
<p>The two sentinel nodes (<code>head</code> and <code>tail</code>) exist purely to avoid null checks at the boundaries — every real node always has a valid <code>prev</code> and <code>next</code> to work with.</p>
<h2>Common Pitfalls</h2>
<p><strong>Forgetting to update the hash map on eviction.</strong> If you remove a node from the linked list but leave its entry in <code>$map</code>, the next <code>get()</code> for that key returns a dangling node instead of a miss.</p>
<p><strong>Off-by-one on capacity checks.</strong> Check <code>count($this-&gt;map) &gt;= $this-&gt;capacity</code> <em>before</em> inserting the new node, not after — otherwise the cache temporarily holds capacity + 1 items.</p>
<p><strong>Treating get() as read-only.</strong> A cache hit still mutates state — it moves the node to the front. Skipping that step turns your LRU into a plain FIFO cache.</p>
<p><strong>Using an array and array_shift() as the "queue."</strong> <code>array_shift()</code> is O(n) because PHP reindexes the array. This defeats the entire point of the exercise — the doubly linked list is what makes eviction O(1).</p>
<h2>When to Reach for a Real Implementation</h2>
<p>Don't hand-roll this for production unless you have a specific reason (embedding the cache inside a single PHP process with no external dependency, for instance). For anything shared across requests or processes, Redis's own eviction policies (<code>allkeys-lru</code>, <code>volatile-lru</code>) or a library like <code>symfony/cache</code> already solve this correctly and durably. The value of building it yourself is understanding <em>why</em> O(1) requires both structures together — neither one alone gets you there.</p>]]></content>
            </entry>
        <entry>
        <title>Idempotency Keys: Preventing Duplicate Payments in Distributed Systems</title>
        <link href="https://cslant.com/tips/idempotency-keys-preventing-duplicate-payments-in-distributed-systems" />
        <id>https://cslant.com/tips/idempotency-keys-preventing-duplicate-payments-in-distributed-systems</id>
        <updated>2026-08-18T09:00:00+07:00</updated>
                        <category term="Laravel" />
                        <summary>Why idempotency keys matter in payment APIs, how to implement them safely with a database-backed store, and common pitfalls like race conditions and key collisions.</summary>
                        <content type="html"><![CDATA[<p>Idempotency keys are the difference between a retried payment request and an accidental double-charge. In any distributed system where a client might retry a request after a timeout — mobile app on flaky wifi, a load balancer failing over, a queue redelivering a message — the server needs a way to recognize "I've already done this" and return the original result instead of repeating the side effect.</p>
<h2>The Problem</h2>
<pre><code>Client -&gt; POST /charge {amount: 5000} -&gt; Server processes charge, DB write succeeds
Client -&gt; (times out waiting for response, retries)
Client -&gt; POST /charge {amount: 5000} -&gt; Server processes AGAIN -&gt; customer charged twice
</code></pre>
<p>The client never knows if the first request failed before or after the side effect happened. Without idempotency, a naive retry strategy silently causes double charges, duplicate emails, duplicate orders.</p>
<h2>The Fix: Idempotency Keys</h2>
<p>The client generates a unique key (usually a UUID) per logical operation and sends it in a header:</p>
<pre><code>POST /api/v1/charges
Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7
{ "amount": 5000, "currency": "USD" }
</code></pre>
<p>The server stores the key alongside the result of the first successful execution. On retry with the same key, it returns the stored result without re-executing the side effect.</p>
<h2>Implementation with a Database-Backed Store</h2>
<pre><code class="language-php">function handleCharge(Request $request) {
    $key = $request-&gt;header('Idempotency-Key');
    if (!$key) {
        abort(400, 'Idempotency-Key header required');
    }

    return DB::transaction(function () use ($key, $request) {
        $existing = IdempotencyKey::where('key', $key)-&gt;lockForUpdate()-&gt;first();

        if ($existing) {
            if ($existing-&gt;status === 'processing') {
                abort(409, 'Request with this key is already being processed');
            }
            return response($existing-&gt;response_body, $existing-&gt;response_status);
        }

        IdempotencyKey::create([
            'key' =&gt; $key,
            'status' =&gt; 'processing',
            'request_hash' =&gt; hash('sha256', $request-&gt;getContent()),
        ]);

        $result = processCharge($request-&gt;all());

        IdempotencyKey::where('key', $key)-&gt;update([
            'status' =&gt; 'completed',
            'response_status' =&gt; 200,
            'response_body' =&gt; json_encode($result),
        ]);

        return response()-&gt;json($result);
    });
}
</code></pre>
<p>The row-level lock (<code>lockForUpdate</code>) inside the transaction is what prevents two concurrent requests with the same key from both slipping past the "does it exist" check — this is the race condition most naive implementations miss.</p>
<h2>Sequence of a Safe Retry</h2>
<div class="page-mermaid"><div class="mermaid">sequenceDiagram
    participant C as Client
    participant S as Server
    participant D as DB

    C-&gt;&gt;S: POST /charge (key=abc123)
    S-&gt;&gt;D: BEGIN, SELECT ... FOR UPDATE (key=abc123)
    D--&gt;&gt;S: not found
    S-&gt;&gt;D: INSERT key=abc123 status=processing
    S-&gt;&gt;S: process charge
    S-&gt;&gt;D: UPDATE key=abc123 status=completed, store response
    S-&gt;&gt;D: COMMIT
    S--&gt;&gt;C: 200 OK

    Note over C,S: network drop, client never saw the 200

    C-&gt;&gt;S: POST /charge (key=abc123) [retry]
    S-&gt;&gt;D: SELECT ... FOR UPDATE (key=abc123)
    D--&gt;&gt;S: found, status=completed
    S--&gt;&gt;C: 200 OK (stored response, no new charge)</div></div>
<h2>Common Pitfalls</h2>
<p><strong>Not hashing the request body.</strong> If a client reuses a key with a <em>different</em> payload (different amount, say), returning the cached response silently applies the wrong charge to the wrong intent. Store a hash of the request body with the key and reject mismatches with a 422.</p>
<p><strong>No expiration.</strong> Idempotency keys should expire (24-48 hours is typical for payment APIs). Keeping them forever bloats the table and risks legitimate reuse of a UUID being blocked incorrectly, however unlikely.</p>
<p><strong>Treating "processing" as "not found."</strong> If a second request arrives while the first is still mid-flight (not a retry after failure, but genuine concurrency), returning 409 (Conflict) or making the client poll is safer than letting both proceed.</p>
<p><strong>Scoping keys per-endpoint, not globally.</strong> The same key sent to two different endpoints should not collide. Compose the lookup on <code>(key, endpoint)</code> or <code>(key, account_id)</code>, not just <code>key</code> alone.</p>
<p><strong>Only applying this to POST.</strong> PUT and PATCH can double-apply too if not naturally idempotent (e.g., "increment balance by 100" instead of "set balance to 100"). Prefer absolute updates over relative ones where money is involved, and use idempotency keys for both.</p>
<h2>When You Don't Need This</h2>
<p>Truly idempotent operations by construction (a PUT that sets absolute state, a GET, a DELETE by ID) don't need a key — retrying them is already safe. Idempotency keys matter specifically when an operation has a side effect that isn't naturally safe to repeat: charging money, sending an email, decrementing inventory, creating a resource with server-generated side effects.</p>
<blockquote>
<p>Rule of thumb: any endpoint that moves money, sends a notification, or creates a resource that shouldn't be duplicated needs an idempotency key. Everything else, question whether you're overengineering.</p>
</blockquote>]]></content>
            </entry>
        <entry>
        <title>Database Deadlocks: Detection, Diagnosis, and Prevention</title>
        <link href="https://cslant.com/tips/database-deadlocks-detection-diagnosis-and-prevention" />
        <id>https://cslant.com/tips/database-deadlocks-detection-diagnosis-and-prevention</id>
        <updated>2026-08-10T09:00:00+07:00</updated>
                        <category term="Database" />
                        <summary>How relational databases detect deadlocks with wait-for graphs, why they happen, and concrete techniques to prevent them in high-concurrency systems.</summary>
                        <content type="html"><![CDATA[<p>A deadlock happens when two or more transactions each hold a lock the other needs, and neither can proceed. Databases don't let this hang forever — they detect it and kill one of the transactions. Understanding how that detection works, and how to avoid triggering it in the first place, is a core skill for anyone writing high-concurrency database code.</p>
<h2>The Classic Deadlock</h2>
<p>Two transactions, two rows, opposite order:</p>
<pre><code class="language-sql">-- Transaction A
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- ... pauses ...
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;

-- Transaction B (running concurrently)
BEGIN;
UPDATE accounts SET balance = balance - 50 WHERE id = 2;
-- ... pauses ...
UPDATE accounts SET balance = balance + 50 WHERE id = 1;
COMMIT;
</code></pre>
<p>If A locks row 1 and B locks row 2 at nearly the same time, A's second statement waits for B to release row 2, while B's second statement waits for A to release row 1. Neither can ever proceed.</p>
<h2>Wait-For Graphs: How Databases Detect This</h2>
<p>Most databases (PostgreSQL, MySQL/InnoDB, SQL Server) model lock waits as a directed graph: each transaction is a node, and an edge from T1 to T2 means "T1 is waiting on a lock held by T2." A deadlock exists exactly when this graph contains a cycle.</p>
<div class="page-mermaid"><div class="mermaid">graph TD
    A[Transaction A] --&gt;|waits for row 2| B[Transaction B]
    B[Transaction B] --&gt;|waits for row 1| A[Transaction A]
    A -.cycle detected.-&gt; C[Deadlock!]
    C --&gt; D[Database picks a victim]
    D --&gt; E[Victim transaction rolled back]
    E --&gt; F[Survivor proceeds]</div></div>
<p>Periodically (or on every new lock wait, depending on the engine), the database walks this graph looking for cycles. When it finds one, it picks a <strong>victim</strong> — usually the transaction that has done the least work, or holds the fewest locks — and rolls it back, releasing its locks so the other transaction can continue.</p>
<h2>What the Error Actually Looks Like</h2>
<pre><code class="language-text">ERROR: deadlock detected
DETAIL: Process 1234 waits for ShareLock on transaction 5678; blocked by process 5678.
Process 5678 waits for ShareLock on transaction 1234; blocked by process 1234.
HINT: See server log for query details.
</code></pre>
<p>The application's job is to catch this specific error and retry the transaction — a deadlock victim is not a bug, it's the database correctly resolving an unavoidable conflict.</p>
<pre><code class="language-python">import time

def run_with_retry(fn, max_attempts=3):
    for attempt in range(max_attempts):
        try:
            return fn()
        except DeadlockDetected:
            if attempt == max_attempts - 1:
                raise
            time.sleep(0.05 * (2 ** attempt))  # exponential backoff
</code></pre>
<h2>Deadlocks vs. Ordinary Lock Waits</h2>
<p>An ordinary lock wait resolves itself once the holding transaction commits or rolls back — no cycle, no error, just a delay. A deadlock is fundamentally different: without intervention, it never resolves on its own, because every party in the cycle is waiting on another party in the same cycle. This is why detection has to be active (scanning for cycles), not passive (just waiting things out).</p>
<h2>Prevention: Consistent Lock Ordering</h2>
<p>The single most effective prevention technique is to always acquire locks in the same order across every transaction that touches the same set of resources. If both transactions above had updated row 1 before row 2, the second one to arrive would simply wait for the first to finish — no cycle possible.</p>
<pre><code class="language-sql">-- Both transactions now lock in ascending id order — no deadlock possible
UPDATE accounts SET balance = balance - 100 WHERE id = LEAST(1, 2);
UPDATE accounts SET balance = balance + 100 WHERE id = GREATEST(1, 2);
</code></pre>
<p>In practice this often means sorting rows by primary key before issuing a batch of updates, or centralizing the lock-acquisition order in a single code path rather than leaving it to each caller.</p>
<h2>Prevention: Keep Transactions Short</h2>
<p>Long-running transactions hold locks longer, which widens the window during which a conflicting transaction can show up and create a cycle. Moving non-essential work (sending emails, calling external APIs, heavy computation) outside the transaction boundary shrinks that window significantly.</p>
<h2>Prevention: Lower Isolation Where Safe</h2>
<p>Higher isolation levels take more locks for longer. <code>SERIALIZABLE</code> is the most deadlock-prone; <code>READ COMMITTED</code> takes fewer, shorter-held locks. Not every operation needs the strongest guarantee — evaluate whether a lower isolation level is safe for a given transaction before defaulting to the strictest one everywhere.</p>
<h2>Prevention: Smaller Lock Footprint</h2>
<p>Locking an entire table when you only need a few rows multiplies the chance of collision. Use indexed WHERE clauses so the database can take row-level (not table-level) locks, and avoid <code>SELECT ... FOR UPDATE</code> over more rows than the transaction actually intends to modify.</p>
<h2>Diagnosing Recurring Deadlocks</h2>
<p>When deadlocks show up repeatedly in production, look at the database's deadlock log (PostgreSQL logs full lock and query detail when <code>log_lock_waits</code> is on; MySQL exposes <code>SHOW ENGINE INNODB STATUS</code>). The recurring pattern is almost always the same two code paths acquiring the same two resources in opposite order — find those two call sites and fix the ordering, rather than just adding retry logic and hoping.</p>
<h2>Common Pitfalls</h2>
<p>Treating deadlocks purely as something to retry around, without ever fixing the underlying lock-order conflict, just moves the cost to increased latency and wasted work under load. Assuming an ORM's default query patterns are deadlock-safe is risky — many ORMs issue related updates in whatever order model associations happen to be traversed, which can vary between requests. And forgetting to add retry logic entirely means a deadlock becomes a user-facing error instead of an invisible, automatically-recovered hiccup.</p>
<blockquote>
<p>A deadlock isn't a failure of the database — it's the database correctly refusing to let two transactions wait on each other forever. The fix lives in your application's lock ordering, not in disabling detection.</p>
</blockquote>]]></content>
            </entry>
        <entry>
        <title>RedGo – Ride-Hailing, Delivery &amp; Food App</title>
        <link href="https://cslant.com/works/redgo-ride-hailing-delivery-food-super-app" />
        <id>https://cslant.com/works/redgo-ride-hailing-delivery-food-super-app</id>
        <updated>2026-07-27T09:00:00+07:00</updated>
                        <category term="Mobile App" />
                        <summary>All-in-one super app landing page for ride-hailing, food &amp; drink delivery, and package delivery in Vietnam — built for RedGo by CSlant × Wztek.</summary>
                        <content type="html"><![CDATA[RedGo is a Vietnam-based app combining ride-hailing, food delivery, package delivery, and drink ordering into a single platform. CSlant partnered with Wztek to design and build RedGo's marketing website simultaneously with its iOS and Android app launch.

The site highlights RedGo's four core services — ride-hailing, food delivery, package delivery, and drinks — through interactive product mockups showing live order tracking, driver matching, and checkout flows. Dedicated sections speak to riders and shoppers as well as prospective drivers and restaurant partners, with clear calls to action to download the app.

Built with a modern, mobile stack and optimized for performance and SEO, the site serves to showcase the RedGo ecosystem, letting people know about our services and guiding them to the mobile app where the core experience takes place.]]></content>
            </entry>
        <entry>
        <title>Distributed Locking with Redis: Understanding the Redlock Algorithm</title>
        <link href="https://cslant.com/tips/distributed-locking-with-redis-understanding-the-redlock-algorithm" />
        <id>https://cslant.com/tips/distributed-locking-with-redis-understanding-the-redlock-algorithm</id>
        <updated>2026-07-24T19:00:00+07:00</updated>
                        <category term="Database" />
                        <summary>A deep dive into the Redlock algorithm for safe distributed locking across multiple Redis nodes, including failure modes, clock drift, and the fencing token fix.</summary>
                        <content type="html"><![CDATA[<p>A single Redis instance makes a fine lock for a single-process app, but the moment you have multiple services racing to acquire the same lock — and that Redis instance can fail — a naive <code>SETNX</code> isn't safe anymore. <strong>Redlock</strong>, proposed by Redis's creator, is an algorithm for acquiring a distributed lock across multiple independent Redis nodes so that no single node's failure or slowness can silently break mutual exclusion.</p>
<blockquote>
<p>This is genuinely advanced material — understanding it requires comfort with distributed systems failure modes, not just Redis commands.</p>
</blockquote>
<h2>Why a Single Redis Lock Isn't Enough</h2>
<p>A basic lock looks simple:</p>
<pre><code class="language-bash">SET resource:order-42 my-random-value NX PX 30000
</code></pre>
<p>This sets the key only if it doesn't exist (<code>NX</code>), with a 30-second expiry (<code>PX</code>) so a crashed client doesn't hold the lock forever. The problem is that this single Redis node is a single point of failure. If it goes down before replicating the write to a replica, and the replica gets promoted to master, another client can acquire the "same" lock — because the new master never saw the key.</p>
<h2>The Redlock Algorithm</h2>
<p>Redlock solves this by using <strong>N independent Redis masters</strong> (the reference implementation uses 5), with no replication or coordination between them. A client wanting the lock does the following:</p>
<ol>
<li>Get the current time in milliseconds.</li>
<li>Try to acquire the lock on all N instances sequentially, using the same key and a random value, with a small timeout per instance so a down node doesn't stall the whole process.</li>
<li>Compute elapsed time. The lock is considered acquired only if the client got the lock on a <strong>majority</strong> (N/2 + 1) of instances, and the total elapsed time is less than the lock's validity time.</li>
<li>If acquired, the effective validity time is the original TTL minus the elapsed time and clock drift.</li>
<li>If the lock wasn't acquired, release it on every instance immediately, whether or not that instance thought it succeeded.</li>
</ol>
<div class="page-mermaid"><div class="mermaid">graph TD
    A[Client wants lock] --&gt; B[Record start time T1]
    B --&gt; C[Try SET NX PX on Redis Node 1]
    B --&gt; D[Try SET NX PX on Redis Node 2]
    B --&gt; E[Try SET NX PX on Redis Node 3]
    B --&gt; F[Try SET NX PX on Redis Node 4]
    B --&gt; G[Try SET NX PX on Redis Node 5]
    C --&gt; H{Count successes}
    D --&gt; H
    E --&gt; H
    F --&gt; H
    G --&gt; H
    H --&gt;|Majority acquired AND time OK| I[Lock acquired]
    H --&gt;|Majority failed OR time expired| J[Release lock on all nodes]
    J --&gt; K[Retry after random backoff]</div></div>
<h2>Why Majority Matters</h2>
<p>Requiring a majority (not all N) means Redlock tolerates the failure of a minority of nodes — with 5 nodes, up to 2 can be down or unreachable and the lock still works correctly, the same fault-tolerance principle behind Raft and Paxos-based systems. It also prevents split-brain: two clients can't both get a majority of 5 nodes simultaneously, because any two majorities of 5 must overlap by at least one node.</p>
<h2>The Random Value and Safe Release</h2>
<p>Each client generates a unique random value for its lock attempt (not just any placeholder). Releasing the lock must check that the stored value still matches before deleting it, done atomically via a Lua script:</p>
<pre><code class="language-lua">if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end
</code></pre>
<p>Without this check, a client could accidentally delete a lock it no longer owns — for example, if its own lock expired and a different client acquired it in the meantime.</p>
<h2>Clock Drift: The Real Weakness</h2>
<p>Redlock's correctness assumes clocks across the N nodes don't drift too far apart relative to the lock's TTL. If a node's clock jumps forward unexpectedly (a bad NTP sync, a VM pause-and-resume, manual clock changes), that node might expire a lock much earlier than the others believe, opening a window where two clients think they hold the lock simultaneously. This is the core of Martin Kleppmann's well-known critique of Redlock: it depends on real-time clock behavior in ways that are hard to fully guarantee.</p>
<h2>Fencing Tokens: The Practical Fix</h2>
<p>Even with Redlock done correctly, a paused client (GC pause, VM suspend, network partition) can wake up after its lock has expired and still act as if it holds it — writing to a shared resource after another client has already acquired the lock and moved on. The standard mitigation is a <strong>fencing token</strong>: a monotonically increasing number returned every time a lock is granted.</p>
<pre><code class="language-text">Client A acquires lock, gets token 33
Client A pauses (GC, network delay...)
Client A's lock expires
Client B acquires lock, gets token 34
Client B writes to storage, tagging the write with token 34
Client A wakes up, tries to write with token 33
Storage rejects it: 33 &lt; 34 (already saw a higher token)
</code></pre>
<p>The protected resource itself — a database, a file store, an API — must be the one enforcing the token check. Redlock alone cannot make this guarantee; it can only reduce the <em>likelihood</em> of two clients believing they hold the lock at once.</p>
<h2>When to Use Redlock (and When Not To)</h2>
<p>Redlock is a reasonable choice for <strong>efficiency locks</strong> — cases where a duplicate execution is wasteful but not catastrophic, like preventing the same cron-triggered job from running twice, or coalescing duplicate cache-rebuild requests. It is a risky choice for <strong>correctness locks</strong> — cases where a duplicate execution corrupts data, like coordinating writes to a financial ledger. For those, use a system with strong consistency guarantees (a consensus-based lock service like ZooKeeper or etcd) combined with fencing tokens enforced by the resource itself, not just Redis.</p>
<h2>Common Pitfalls</h2>
<p>Using an even number of nodes defeats the majority-quorum logic that makes Redlock resilient — always use an odd number so a majority is unambiguous. Setting per-node acquisition timeouts too high means a single slow node can blow past your lock's TTL before you've even finished the acquisition phase. And treating Redlock as a strict correctness guarantee, rather than a best-effort mutual exclusion mechanism, is the mistake that leads to production incidents — pair it with fencing tokens whenever the operation it protects actually matters.</p>
<blockquote>
<p>Redlock reduces the probability of a broken lock; it does not eliminate it. Design your protected operations to survive the case where the lock briefly fails.</p>
</blockquote>]]></content>
            </entry>
        <entry>
        <title>Git Interactive Rebase: Clean Up Your Commit History Before a PR</title>
        <link href="https://cslant.com/tips/git-interactive-rebase-clean-up-your-commit-history-before-a-pr" />
        <id>https://cslant.com/tips/git-interactive-rebase-clean-up-your-commit-history-before-a-pr</id>
        <updated>2026-07-23T18:15:00+07:00</updated>
                        <category term="DevOps" />
                        <summary>Learn how to use git rebase -i to squash, reorder, and edit commits so your pull requests tell a clean, reviewable story.</summary>
                        <content type="html"><![CDATA[<p>Every developer who's shipped a "WIP", "fix typo", "actually fix it", "oops" string of commits knows the pain of opening a pull request and seeing a commit history that tells no story at all. <strong>Interactive rebase</strong> is the tool that turns that mess into a clean, linear narrative your reviewers can actually follow.</p>
<h2>Why Clean History Matters</h2>
<p>A pull request's commit history isn't just a log — it's documentation. Future developers (including you, in six months) will use <code>git blame</code> and <code>git log</code> to understand <em>why</em> a change was made. A history full of noise makes that archaeology painful.</p>
<blockquote>
<p>A clean commit history is a gift to your future self and your reviewers.</p>
</blockquote>
<h2>The Basic Command</h2>
<pre><code class="language-bash">git rebase -i HEAD~5
</code></pre>
<p>This opens your last 5 commits in an editor, each prefixed with <code>pick</code>. From here you can reorder lines, or swap <code>pick</code> for one of several rebase commands.</p>
<h2>The Rebase Commands</h2>
<table>
<thead>
<tr>
<th>Command</th>
<th>Shorthand</th>
<th>Effect</th>
</tr>
</thead>
<tbody>
<tr>
<td>pick</td>
<td>p</td>
<td>Keep the commit as-is</td>
</tr>
<tr>
<td>reword</td>
<td>r</td>
<td>Keep changes, edit the commit message</td>
</tr>
<tr>
<td>edit</td>
<td>e</td>
<td>Pause here to amend the commit</td>
</tr>
<tr>
<td>squash</td>
<td>s</td>
<td>Merge into the previous commit, combine messages</td>
</tr>
<tr>
<td>fixup</td>
<td>f</td>
<td>Merge into the previous commit, discard this message</td>
</tr>
<tr>
<td>drop</td>
<td>d</td>
<td>Remove the commit entirely</td>
</tr>
</tbody>
</table>
<h2>How It Works Under the Hood</h2>
<p>Rebase doesn't move commits — it replays them. Git walks your branch's commits one at a time, applying each as a patch on top of the new base, and builds brand-new commit objects (with new SHAs) along the way.</p>
<div class="page-mermaid"><div class="mermaid">graph TD
    A[Start: git rebase -i HEAD~5] --&gt; B[Git opens commit list in editor]
    B --&gt; C{You edit each line}
    C --&gt;|pick| D[Commit kept as-is]
    C --&gt;|reword| E[Edit message only]
    C --&gt;|squash/fixup| F[Merge into previous commit]
    C --&gt;|drop| G[Commit removed]
    D --&gt; H[Git replays commits onto new base]
    E --&gt; H
    F --&gt; H
    G --&gt; H
    H --&gt; I[New commit history with new SHAs]
    I --&gt; J[Force-push to update remote branch]</div></div>
<h2>A Real Example</h2>
<p>Say your branch has this history before opening a PR:</p>
<pre><code class="language-text">a1b2c3d WIP
e4f5g6h fix typo
h7i8j9k actually add the feature
k0l1m2n oops forgot a file
</code></pre>
<p>Run <code>git rebase -i HEAD~4</code>, then change the plan to:</p>
<pre><code class="language-text">pick h7i8j9k actually add the feature
fixup k0l1m2n oops forgot a file
fixup a1b2c3d WIP
fixup e4f5g6h fix typo
</code></pre>
<p>Save and exit — Git squashes all four into a single, well-described commit.</p>
<h2>Editing a Commit Message Mid-Rebase</h2>
<p>Mark a commit <code>reword</code>, and after saving the plan, Git will stop and open your editor just for that commit's message:</p>
<pre><code class="language-bash">git rebase -i HEAD~3
# change 'pick' to 'reword' on the line you want to rename
# Git opens your editor — update the message, save, close
# rebase continues automatically
</code></pre>
<h2>Splitting or Amending a Commit</h2>
<p>Mark a commit <code>edit</code> to pause the rebase right after it's applied:</p>
<pre><code class="language-bash">git rebase -i HEAD~3
# mark the target commit as 'edit'

# rebase pauses here — make your changes
git add &lt;file&gt;
git commit --amend

# then continue
git rebase --continue
</code></pre>
<h2>Handling Conflicts</h2>
<p>Since rebase replays commits one by one, conflicts can surface at any step:</p>
<pre><code class="language-bash"># fix the conflicting files
git add &lt;resolved-files&gt;
git rebase --continue

# or bail out entirely and go back to where you started
git rebase --abort
</code></pre>
<h2>Pushing Rebased History</h2>
<p>Because rebase rewrites commit SHAs, a normal <code>git push</code> will be rejected. You need a force push — but use the safer variant:</p>
<pre><code class="language-bash">git push --force-with-lease
</code></pre>
<p><code>--force-with-lease</code> refuses to overwrite the remote branch if someone else has pushed to it since your last fetch, protecting teammates from losing work — unlike a plain <code>--force</code>.</p>
<h2>When to Use Which</h2>
<p>Use <strong>squash</strong> when a commit's changes belong with the previous one but the message itself still adds context worth keeping. Use <strong>fixup</strong> for pure "oops" commits where the message adds nothing. Use <strong>reword</strong> when the code is fine but the message is misleading or too terse. Use <strong>drop</strong> for commits that turned out to be dead ends, like an experiment you abandoned.</p>
<h2>Common Pitfalls</h2>
<p>Never rebase commits that other people have already pulled and built on top of — you'll rewrite history out from under them and create painful merge conflicts on their end. Rebase is for <strong>local or PR-branch cleanup before merge</strong>, not for branches already shared and built upon. Also double-check your rebase plan before saving; a misplaced <code>drop</code> silently deletes a commit's changes, though <code>git reflog</code> can usually recover it if you catch the mistake quickly.</p>
<blockquote>
<p>Interactive rebase rewrites history — that's exactly why it's powerful, and exactly why it should stay confined to branches only you (or your PR) are working on.</p>
</blockquote>]]></content>
            </entry>
        <entry>
        <title>Redis Caching Strategies: Cache-Aside vs Write-Through</title>
        <link href="https://cslant.com/tips/redis-caching-strategies-cache-aside-vs-write-through" />
        <id>https://cslant.com/tips/redis-caching-strategies-cache-aside-vs-write-through</id>
        <updated>2026-07-22T18:30:00+07:00</updated>
                        <category term="Database" />
                        <summary>Compare cache-aside and write-through caching with Redis, including consistency trade-offs, TTL strategy, and when to use each pattern.</summary>
                        <content type="html"><![CDATA[<p>Redis makes caching easy to bolt on, but the pattern you choose determines how consistent, resilient, and performant your system actually is. The two most common patterns — <strong>cache-aside</strong> and <strong>write-through</strong> — solve the same problem in fundamentally different ways.</p>
<h2>Cache-Aside (Lazy Loading)</h2>
<p>In cache-aside, the application talks to the cache directly, and the cache stays passive. On a read, the app checks Redis first; on a miss, it loads from the database and populates the cache. Writes go straight to the database, and the cache entry is invalidated (or updated) afterward.</p>
<pre><code class="language-python">def get_user(user_id):
    cached = redis.get(f"user:{user_id}")
    if cached:
        return json.loads(cached)

    user = db.query("SELECT * FROM users WHERE id = %s", user_id)
    redis.setex(f"user:{user_id}", 3600, json.dumps(user))
    return user

def update_user(user_id, data):
    db.execute("UPDATE users SET ... WHERE id = %s", user_id)
    redis.delete(f"user:{user_id}")  # invalidate, don't update
</code></pre>
<h2>Write-Through</h2>
<p>In write-through, the cache sits in front of the database and owns the write path. Every write goes to the cache first, which synchronously writes through to the database before returning. Reads always come from the cache since it's guaranteed to be up to date.</p>
<pre><code class="language-python">def update_user(user_id, data):
    db.execute("UPDATE users SET ... WHERE id = %s", user_id)
    redis.setex(f"user:{user_id}", 3600, json.dumps(data))  # update, not invalidate
    return data
</code></pre>
<h2>How the Two Patterns Differ</h2>
<div class="page-mermaid"><div class="mermaid">graph TD
    A[Application] --&gt;|Read| B{Cache-Aside}
    B --&gt;|Hit| C[Return from Redis]
    B --&gt;|Miss| D[Query Database]
    D --&gt; E[Populate Redis]
    E --&gt; C

    F[Application] --&gt;|Write| G{Write-Through}
    G --&gt; H[Write to Redis]
    H --&gt; I[Write to Database]
    I --&gt; J[Confirm to App]</div></div>
<h2>Consistency Trade-offs</h2>
<p>Cache-aside has a small window where the cache can serve stale data: if two requests race between the database write and the cache invalidation, a reader might get an old value. Write-through avoids this because the cache and database are updated together in the same operation — but that comes at the cost of every write now waiting on two systems instead of one.</p>
<blockquote>
<p>Cache-aside optimizes for read throughput and simplicity. Write-through optimizes for consistency at the cost of write latency.</p>
</blockquote>
<h2>TTL Strategy</h2>
<p>Even with write-through, set a TTL as a safety net — bugs, race conditions, and out-of-band database writes (migrations, admin scripts) can all leave the cache holding stale data indefinitely without one.</p>
<pre><code class="language-python">CACHE_TTL_SECONDS = 3600  # 1 hour

redis.setex(key, CACHE_TTL_SECONDS, value)
</code></pre>
<p>A short TTL (minutes) suits volatile data like inventory counts or session state. A longer TTL (hours) suits data that changes rarely, like user profiles or product catalogs.</p>
<h2>Handling Cache Stampedes</h2>
<p>When a hot key expires, dozens of concurrent requests can all miss the cache at once and hammer the database simultaneously. A simple mitigation is a short-lived lock so only one request repopulates the cache while the rest wait:</p>
<pre><code class="language-python">def get_user(user_id):
    cached = redis.get(f"user:{user_id}")
    if cached:
        return json.loads(cached)

    lock = redis.set(f"lock:user:{user_id}", "1", nx=True, ex=5)
    if lock:
        user = db.query("SELECT * FROM users WHERE id = %s", user_id)
        redis.setex(f"user:{user_id}", 3600, json.dumps(user))
        redis.delete(f"lock:user:{user_id}")
        return user
    else:
        time.sleep(0.05)
        return get_user(user_id)  # retry, likely a hit now
</code></pre>
<h2>Write-Behind: A Third Option</h2>
<p>Worth knowing, though outside this tip's main comparison: <strong>write-behind</strong> (write-back) caching writes to Redis immediately and asynchronously flushes to the database later. It gives the lowest write latency but risks data loss if the cache crashes before flushing — generally reserved for high-throughput analytics or logging pipelines, not systems of record.</p>
<h2>When to Use Which</h2>
<p>Reach for <strong>cache-aside</strong> when reads vastly outnumber writes and brief staleness is acceptable — product listings, user profiles, configuration data. Reach for <strong>write-through</strong> when correctness matters more than write speed and you can't tolerate cache/database drift — pricing data, inventory levels, anything downstream systems depend on being accurate.</p>
<h2>Common Pitfalls</h2>
<p>Forgetting to invalidate the cache on delete, not just update, leaves ghost entries that outlive the data they represent. Using a single global TTL for wildly different data types wastes memory on rarely-changing data and creates stampedes on frequently-changing data with the same expiry window. And treating Redis as a source of truth instead of a cache is a design smell — if losing the cache would lose data, it isn't a cache anymore.</p>
<blockquote>
<p>The right caching strategy is the one whose failure mode you can live with: cache-aside fails toward staleness, write-through fails toward latency. Pick based on what your system can actually tolerate.</p>
</blockquote>]]></content>
            </entry>
        <entry>
        <title>TypeScript Utility Types: Pick, Omit, and Partial Explained</title>
        <link href="https://cslant.com/tips/typescript-utility-types-pick-omit-and-partial-explained" />
        <id>https://cslant.com/tips/typescript-utility-types-pick-omit-and-partial-explained</id>
        <updated>2026-07-22T17:42:00+07:00</updated>
                        <category term="JavaScript" />
                        <summary>Stop duplicating interfaces for every slightly-different shape. Pick, Omit, and Partial let you derive new types from one source of truth — here&#039;s when to reach for each.</summary>
                        <content type="html"><![CDATA[<h1>TypeScript Utility Types: Pick, Omit, and Partial Explained</h1>
<p>Every API needs a slightly different shape of the same data: the full <code>User</code>, a public-safe <code>User</code> without the password hash, a form that only updates two fields. Copy-pasting the interface for each case means they drift apart the moment one gets updated. TypeScript's built-in utility types solve this by deriving new types from one source of truth.</p>
<h2>The Problem</h2>
<pre><code class="language-ts">interface User {
  id: string
  name: string
  email: string
  passwordHash: string
  createdAt: Date
}

// Duplicated, drifts out of sync with User over time
interface PublicUser {
  id: string
  name: string
  email: string
  createdAt: Date
}
</code></pre>
<p>Every time <code>User</code> changes, someone has to remember to update <code>PublicUser</code> too. They won't.</p>
<h2>Partial&lt;T&gt;: Make Every Field Optional</h2>
<p>Perfect for update payloads, where the caller only sends the fields that changed.</p>
<pre><code class="language-ts">function updateUser(id: string, changes: Partial&lt;User&gt;) {
  // changes might be { name: 'New Name' } or { email: '...' }
  return db.users.update(id, changes)
}
</code></pre>
<h2>Pick&lt;T, K&gt;: Select a Subset of Fields</h2>
<p>Use it when you only need a few fields from a larger type — a preview card, a dropdown option.</p>
<pre><code class="language-ts">type UserPreview = Pick&lt;User, 'id' | 'name'&gt;
// { id: string; name: string }
</code></pre>
<h2>Omit&lt;T, K&gt;: Exclude Specific Fields</h2>
<p>The mirror image of <code>Pick</code> — keep everything except what you list. Ideal for stripping sensitive fields before sending a response.</p>
<pre><code class="language-ts">type PublicUser = Omit&lt;User, 'passwordHash'&gt;
// { id: string; name: string; email: string; createdAt: Date }
</code></pre>
<p>Now <code>PublicUser</code> always tracks <code>User</code> automatically. Add a field to <code>User</code>, and it shows up in <code>PublicUser</code> without touching this line.</p>
<h2>Combining Them</h2>
<p>Utility types compose. A "rename form" that only lets you edit the name:</p>
<pre><code class="language-ts">type RenameForm = Partial&lt;Pick&lt;User, 'name'&gt;&gt;
// { name?: string }
</code></pre>
<h2>How the Types Derive From One Source</h2>
<div class="page-mermaid"><div class="mermaid">graph TD
    A["User (source of truth)"] --&gt; B["Partial&amp;lt;User&amp;gt;&lt;br/&gt;all fields optional"]
    A --&gt; C["Pick&amp;lt;User, 'id' | 'name'&amp;gt;&lt;br/&gt;only listed fields"]
    A --&gt; D["Omit&amp;lt;User, 'passwordHash'&amp;gt;&lt;br/&gt;everything except listed"]
    C --&gt; E["Partial&amp;lt;Pick&amp;lt;User,...&amp;gt;&amp;gt;&lt;br/&gt;subset, all optional"]</div></div>
<h2>When to Use Which</h2>
<ul>
<li><strong>Partial&lt;T&gt;</strong>: PATCH/update payloads, optional config objects, form state before submission.</li>
<li><strong>Pick&lt;T, K&gt;</strong>: previews, dropdown options, anywhere you need a deliberately small, explicit subset.</li>
<li><strong>Omit&lt;T, K&gt;</strong>: stripping sensitive or internal fields (passwords, internal IDs) while keeping everything else in sync automatically.</li>
</ul>
<h2>Common Pitfalls</h2>
<ul>
<li><strong>Omit with a renamed/removed key silently succeeds</strong> — TypeScript won't error if you omit a key that was already removed from the base type, so typos in the key list can go unnoticed. Double-check the literal keys against the source type.</li>
<li><strong>Partial hides genuinely required fields</strong> — a <code>Partial&lt;User&gt;</code> update function can't tell "the caller forgot <code>id</code>" from "the caller doesn't want to change <code>id</code>." Keep required identifiers (like <code>id</code>) outside the <code>Partial</code> wrapper: <code>{ id: string } &amp; Partial&lt;Omit&lt;User, 'id'&gt;&gt;</code>.</li>
<li><strong>Reaching for <code>Record&lt;string, unknown&gt;</code></strong> instead of <code>Pick</code>/<code>Omit</code> throws away type safety entirely — utility types keep the connection to the original shape, a loose <code>Record</code> doesn't.</li>
</ul>
<blockquote>
<p>Derive, don't duplicate. If two types describe the same entity, one of them should be built from the other with <code>Pick</code>, <code>Omit</code>, or <code>Partial</code> — not maintained by hand.</p>
</blockquote>]]></content>
            </entry>
        <entry>
        <title>Rate Limiting APIs: Token Bucket vs Sliding Window</title>
        <link href="https://cslant.com/tips/rate-limiting-apis-token-bucket-vs-sliding-window" />
        <id>https://cslant.com/tips/rate-limiting-apis-token-bucket-vs-sliding-window</id>
        <updated>2026-07-21T23:09:00+07:00</updated>
                        <category term="Security" />
                        <summary>Fixed windows let clients burst 2x at the boundary. See how token bucket and sliding window rate limiting actually work, with Redis-backed implementations and the pitfalls that break them in production.</summary>
                        <content type="html"><![CDATA[<h1>Rate Limiting APIs: Token Bucket vs Sliding Window</h1>
<p>Without rate limiting, one noisy client can degrade an API for everyone else. But the naive approach to rate limiting has a nasty boundary bug that most teams don't discover until production.</p>
<h2>The Problem: Fixed Window Counter</h2>
<p>The simplest approach counts requests in a fixed time box and resets the counter every period:</p>
<pre><code class="language-js">// Naive fixed window with Redis
async function isAllowed(userId) {
  const key = `rl:${userId}:${Math.floor(Date.now() / 60000)}` // per-minute bucket
  const count = await redis.incr(key)
  await redis.expire(key, 60)
  return count &lt;= 100 // 100 req/min
}
</code></pre>
<p>This looks fine until a client sends 100 requests at 0:59 and another 100 at 1:00 — 200 requests in two seconds, twice the intended limit, simply because they straddle the window boundary.</p>
<h2>Token Bucket</h2>
<p>A bucket holds up to <code>N</code> tokens. Tokens refill at a fixed rate. Every request consumes one token; if the bucket is empty, the request is rejected. This allows short bursts (up to the bucket size) while still enforcing a strict average rate over time — it's what Stripe and AWS API Gateway use under the hood.</p>
<pre><code class="language-js">class TokenBucket {
  constructor(capacity, refillPerSecond) {
    this.capacity = capacity
    this.tokens = capacity
    this.refillPerSecond = refillPerSecond
    this.lastRefill = Date.now()
  }

  tryConsume() {
    this.refill()
    if (this.tokens &lt; 1) return false
    this.tokens -= 1
    return true
  }

  refill() {
    const now = Date.now()
    const elapsedSeconds = (now - this.lastRefill) / 1000
    this.tokens = Math.min(this.capacity, this.tokens + elapsedSeconds * this.refillPerSecond)
    this.lastRefill = now
  }
}
</code></pre>
<h2>Sliding Window Counter</h2>
<p>A cheaper alternative to a full sliding log: weight the previous window's count by how much of it still "overlaps" the current moment.</p>
<pre><code class="language-js">// weighted average of previous + current fixed windows
const weight = 1 - (elapsedInCurrentWindow / windowSize)
const estimatedCount = previousWindowCount * weight + currentWindowCount
</code></pre>
<p>This smooths out the boundary burst problem without the memory cost of storing a timestamp per request.</p>
<h2>How a Rate-Limited Request Flows</h2>
<div class="page-mermaid"><div class="mermaid">graph TD
    A[Incoming Request] --&gt; B{Rate Limiter Check}
    B --&gt; C[Load Bucket State from Redis]
    C --&gt; D{Tokens Available?}
    D --&gt;|Yes| E[Consume Token]
    E --&gt; F[Forward Request to API]
    F --&gt; G[Return 200 + Rate Limit Headers]
    D --&gt;|No| H[Reject Request]
    H --&gt; I[Return 429 Too Many Requests]
    I --&gt; J[Set Retry-After Header]
    K[Background Refill Timer] --&gt; C</div></div>
<h2>When to Use Which</h2>
<ul>
<li><strong>Fixed window</strong>: simplest to implement, fine for coarse per-day/per-hour limits where bursts don't matter.</li>
<li><strong>Token bucket</strong>: the default choice for public APIs — allows reasonable bursts while holding a strict average rate.</li>
<li><strong>Sliding window</strong>: most accurate under sustained load, worth the extra complexity for strict SLAs or billing-sensitive limits.</li>
</ul>
<h2>Common Pitfalls</h2>
<ul>
<li><strong>Limiting by IP alone</strong> breaks for users behind shared NAT or corporate proxies — key on API key or user ID as well as IP.</li>
<li><strong>Skipping rate-limit headers</strong> (<code>X-RateLimit-Remaining</code>, <code>Retry-After</code>) forces clients to guess when they can retry, causing thundering-herd retries.</li>
<li><strong>Per-instance in-memory counters</strong> in a multi-server deployment mean each instance enforces the limit independently — a client can get N times the intended limit by hitting N different instances. Always back the counter with a shared store like Redis.</li>
<li><strong>Not rate-limiting authentication endpoints</strong> separately — login/signup routes need tighter limits than general API traffic to slow down credential stuffing.</li>
</ul>
<blockquote>
<p>Start with a token bucket backed by Redis. It's simple to reason about, allows reasonable bursts, and the same primitive scales from a single endpoint to a full API gateway.</p>
</blockquote>]]></content>
            </entry>
        <entry>
        <title>Next.js App Router: Server Components vs Client Components</title>
        <link href="https://cslant.com/tips/nextjs-app-router-server-components-vs-client-components" />
        <id>https://cslant.com/tips/nextjs-app-router-server-components-vs-client-components</id>
        <updated>2026-07-14T12:42:00+07:00</updated>
                        <category term="Next.js" />
                        <summary>Server Components run only on the server; Client Components hydrate in the browser. See how Next.js App Router renders both, and when to actually reach for &#039;use client&#039;.</summary>
                        <content type="html"><![CDATA[<h1>Next.js App Router: Server Components vs Client Components</h1>
<p>In the Next.js App Router (13+), every component is a <strong>Server Component</strong> by default. This is a big shift from the old Pages Router, and it's the single most misunderstood part of modern Next.js.</p>
<h2>The Problem</h2>
<p>Most developers coming from Create React App or the Pages Router slap <code>'use client'</code> at the top of every file "just in case." That defeats the purpose of Server Components: you end up shipping the same amount of JavaScript to the browser as before, just with extra steps.</p>
<h2>Server Components (the default)</h2>
<p>Server Components render <strong>only on the server</strong>. No JS for them is sent to the browser. They can <code>await</code> data directly, touch a database, or use secrets safely, because that code never leaves the server.</p>
<pre><code class="language-tsx">// app/products/page.tsx
export default async function ProductsPage() {
  const res = await fetch('https://api.example.com/products', {
    next: { revalidate: 60 },
  })
  const products = await res.json()

  return (
    &lt;ul&gt;
      {products.map((p: { id: string; name: string }) =&gt; (
        &lt;li key={p.id}&gt;{p.name}&lt;/li&gt;
      ))}
    &lt;/ul&gt;
  )
}
</code></pre>
<p>No <code>useState</code>, no <code>useEffect</code>, no event handlers here — and no client-side fetch waterfall either.</p>
<h2>Client Components (<code>'use client'</code>)</h2>
<p>Client Components are needed for interactivity: hooks, event listeners, browser-only APIs (<code>window</code>, <code>localStorage</code>), or third-party libraries that rely on the DOM.</p>
<pre><code class="language-tsx">'use client'

import { useState } from 'react'

export default function LikeButton() {
  const [liked, setLiked] = useState(false)

  return (
    &lt;button onClick={() =&gt; setLiked(!liked)}&gt;
      {liked ? 'Liked' : 'Like'}
    &lt;/button&gt;
  )
}
</code></pre>
<p>The <code>'use client'</code> directive marks the <strong>boundary</strong> — everything imported below it also ships to the browser, so keep these components small and push them to the leaves of the tree.</p>
<h2>How Rendering Actually Flows</h2>
<p>On every request, the flow looks like this:</p>
<div class="page-mermaid"><div class="mermaid">graph TD
    A[Browser Request] --&gt; B[Next.js Server]
    B --&gt; C[Render Server Components]
    C --&gt; D[Fetch Data / DB / Secrets]
    D --&gt; E[Generate HTML + RSC Payload]
    E --&gt; F[Send Response to Browser]
    F --&gt; G[Paint HTML Immediately]
    F --&gt; H[Download Client JS Bundle]
    H --&gt; I[Hydrate Client Components Only]
    G --&gt; J[Fully Interactive Page]
    I --&gt; J</div></div>
<p>Two things happen in parallel once the response lands: the static HTML paints right away (fast first contentful paint), and only the Client Component "islands" get hydrated with JS. The Server Component output is never re-executed or hydrated in the browser.</p>
<h2>When to Use Which</h2>
<ul>
<li><strong>Server Component (default)</strong>: fetching data, reading from a database, using API keys/secrets, rendering static or mostly-static UI, reducing bundle size.</li>
<li><strong>Client Component (<code>'use client'</code>)</strong>: <code>useState</code>/<code>useReducer</code>, <code>useEffect</code>, event handlers (<code>onClick</code>, <code>onChange</code>), browser APIs, context providers, third-party UI libraries that touch the DOM.</li>
</ul>
<h2>Common Pitfalls</h2>
<ul>
<li><strong>Marking a whole page <code>'use client'</code></strong> because of one interactive button — push <code>'use client'</code> down to that button only, not the entire page.</li>
<li><strong>Importing a Server Component into a Client Component</strong> doesn't work the way you'd expect — pass the Server Component in as <code>children</code> or a prop instead of importing it directly.</li>
<li><strong>Fetching secrets inside a Client Component</strong> leaks them into the browser bundle. Keep API keys and DB calls in Server Components only.</li>
<li><strong>Forgetting <code>next: { revalidate }</code></strong> on <code>fetch</code> calls, which silently opts routes into fully static rendering when you actually wanted fresh data.</li>
</ul>
<blockquote>
<p>Default to Server Components everywhere. Only add <code>'use client'</code> at the smallest leaf component that truly needs interactivity — your bundle size (and your users) will thank you.</p>
</blockquote>]]></content>
            </entry>
        <entry>
        <title>Debounce vs Throttle in JavaScript: Control How Often Code Runs</title>
        <link href="https://cslant.com/tips/debounce-vs-throttle-in-javascript-control-how-often-code-runs" />
        <id>https://cslant.com/tips/debounce-vs-throttle-in-javascript-control-how-often-code-runs</id>
        <updated>2026-07-09T07:25:56+07:00</updated>
                        <category term="JavaScript" />
                        <summary>Scroll, resize, and keystroke events fire dozens of times a second. Learn how debounce and throttle rate-limit them — and which one to reach for.</summary>
                        <content type="html"><![CDATA[<p>Some events fire dozens of times a second — <code>scroll</code>, <code>resize</code>, <code>mousemove</code>, and every keystroke in a search box. If you run expensive work (an API call, a layout recalculation) on each one, you flood the main thread and the network. <strong>Debounce</strong> and <strong>throttle</strong> are two rate-limiting techniques that fix this — and they solve <em>different</em> problems.</p>
<h2>Debounce: wait until the storm passes</h2>
<p>A debounced function delays running until events <em>stop</em> coming for a set period. Every new event resets the timer, so it fires only once, after things go quiet.</p>
<p>Use it when you only care about the <strong>final</strong> state:</p>
<ul>
<li>Search-as-you-type (call the API after the user stops typing)</li>
<li>Validating a field after the user finishes editing</li>
<li>Autosaving a draft once edits pause</li>
</ul>
<pre><code class="language-js">function debounce(fn, delay = 300) {
  let timer;
  return (...args) =&gt; {
    clearTimeout(timer);
    timer = setTimeout(() =&gt; fn.apply(this, args), delay);
  };
}

const search = debounce((q) =&gt; fetch(`/api/search?q=${q}`), 300);
input.addEventListener('input', (e) =&gt; search(e.target.value));
</code></pre>
<p>Type "laravel" quickly and only <strong>one</strong> request goes out — 300 ms after the last keystroke.</p>
<h2>Throttle: run at a steady rate</h2>
<p>A throttled function runs at most once per interval, no matter how many events fire. It doesn't wait for quiet — it guarantees a regular cadence.</p>
<p>Use it when you want <strong>periodic</strong> updates during a continuous stream:</p>
<ul>
<li>Updating a scroll-progress bar</li>
<li>Recomputing layout on <code>resize</code></li>
<li>Rate-limiting rapid clicks</li>
</ul>
<pre><code class="language-js">function throttle(fn, interval = 200) {
  let last = 0;
  return (...args) =&gt; {
    const now = Date.now();
    if (now - last &gt;= interval) {
      last = now;
      fn.apply(this, args);
    }
  };
}

const onScroll = throttle(() =&gt; updateProgressBar(), 200);
window.addEventListener('scroll', onScroll);
</code></pre>
<p>Scroll for 3 seconds and the bar updates ~15 times (every 200 ms), not on all 100+ scroll events.</p>
<h2>Which one do you need?</h2>
<p>Ask yourself: do you want the result <strong>after</strong> activity settles, or <strong>during</strong> it?</p>
<ul>
<li>"After it settles" → <strong>debounce</strong> (search, autosave, validation).</li>
<li>"At a steady rate while it happens" → <strong>throttle</strong> (scroll, resize, drag).</li>
</ul>
<p>In production, reach for a battle-tested implementation like <code>lodash.debounce</code> / <code>lodash.throttle</code> — they handle leading/trailing edges, cancellation, and <code>maxWait</code> for you.</p>]]></content>
            </entry>
        <entry>
        <title>Database Indexes: Turn a Full Scan Into an Instant Lookup</title>
        <link href="https://cslant.com/tips/database-indexes-turn-a-full-scan-into-an-instant-lookup" />
        <id>https://cslant.com/tips/database-indexes-turn-a-full-scan-into-an-instant-lookup</id>
        <updated>2026-07-08T11:21:45+07:00</updated>
                        <category term="Database" />
                        <summary>See why a query without an index scans every row, how an index turns it into a fast lookup, and how to read EXPLAIN to tell the difference.</summary>
                        <content type="html"><![CDATA[<h1>Database Indexes: Turn a Full Scan Into an Instant Lookup</h1>
<p>A query that feels instant on 1,000 rows can crawl on 1,000,000. The usual reason: the database is doing a <strong>full table scan</strong> — reading every row to find the few you asked for. An <strong>index</strong> fixes that.</p>
<h2>Without an index: a full table scan</h2>
<pre><code class="language-sql">SELECT * FROM users WHERE email = 'ann@example.com';
</code></pre>
<p>With no index on <code>email</code>, the engine reads <strong>every</strong> row and compares <code>email</code>. On a large table that is O(n) work.</p>
<h2>With an index: a lookup</h2>
<pre><code class="language-sql">CREATE INDEX idx_users_email ON users (email);
</code></pre>
<p>An index is a sorted structure (usually a B-tree). The engine now navigates straight to the matching rows in about O(log n) — a handful of reads instead of millions.</p>
<h2>Read the plan with EXPLAIN</h2>
<pre><code class="language-sql">EXPLAIN SELECT * FROM users WHERE email = 'ann@example.com';
</code></pre>
<ul>
<li><code>type = ALL</code> (and "Using where") means a full scan — slow.</li>
<li><code>type = ref</code> or <code>const</code> with a <code>key</code> listed means the index is used — good.</li>
</ul>
<h2>Composite indexes follow a left-to-right rule</h2>
<pre><code class="language-sql">CREATE INDEX idx_orders_user_status ON orders (user_id, status);
</code></pre>
<p>This helps <code>WHERE user_id = ?</code> and <code>WHERE user_id = ? AND status = ?</code>, but <strong>not</strong> <code>WHERE status = ?</code> on its own — a query must use a <strong>left prefix</strong> of the indexed columns.</p>
<h2>When an index does NOT help</h2>
<ul>
<li><code>WHERE YEAR(created_at) = 2026</code> — wrapping the column in a function defeats the index. Use a range instead: <code>created_at &gt;= '2026-01-01' AND created_at &lt; '2027-01-01'</code>.</li>
<li><code>LIKE '%term'</code> with a leading wildcard cannot use a normal index.</li>
<li>Very low-selectivity columns (like a boolean) — a scan is sometimes cheaper than the index.</li>
</ul>
<h2>Key takeaways</h2>
<ul>
<li>No index means a full scan (O(n)); the right index means a lookup (O(log n)).</li>
<li>Index the columns you filter, join and sort on — not every column (indexes cost writes and storage).</li>
<li>Use <code>EXPLAIN</code> to confirm the index is actually used.</li>
<li>Keep filters sargable: never wrap an indexed column in a function.</li>
</ul>]]></content>
            </entry>
        <entry>
        <title>Preventing SQL Injection in PHP</title>
        <link href="https://cslant.com/tips/preventing-sql-injection-php" />
        <id>https://cslant.com/tips/preventing-sql-injection-php</id>
        <updated>2026-06-23T14:41:52+07:00</updated>
                        <category term="PHP" />
                        <summary>Learn essential techniques to protect your PHP applications from SQL injection attacks.</summary>
                        <content type="html"><![CDATA[<h1>Preventing SQL Injection in PHP</h1>
<p>SQL injection is one of the most dangerous security vulnerabilities. Here's how to prevent it.</p>
<h2>❌ Never Do This</h2>
<pre><code class="language-php">// VULNERABLE CODE - DO NOT USE!
$userId = $_GET['id'];
$query = "SELECT * FROM users WHERE id = $userId";
$result = mysqli_query($conn, $query);
</code></pre>
<p>An attacker can inject: <code>?id=1 OR 1=1</code> to access all records!</p>
<h2>✅ Use Prepared Statements (MySQLi)</h2>
<pre><code class="language-php">$userId = $_GET['id'];
$stmt = $conn-&gt;prepare("SELECT * FROM users WHERE id = ?");
$stmt-&gt;bind_param("i", $userId);
$stmt-&gt;execute();
$result = $stmt-&gt;get_result();
</code></pre>
<h2>✅ Use PDO with Prepared Statements</h2>
<pre><code class="language-php">$userId = $_GET['id'];
$stmt = $pdo-&gt;prepare("SELECT * FROM users WHERE id = :id");
$stmt-&gt;execute(['id' =&gt; $userId]);
$user = $stmt-&gt;fetch();
</code></pre>
<h2>✅ Laravel Query Builder (Automatic Protection)</h2>
<pre><code class="language-php">$userId = request('id');
$user = DB::table('users')-&gt;where('id', $userId)-&gt;first();
// or
$user = User::find($userId);
</code></pre>
<h2>Warning: Raw Queries</h2>
<p>Even in Laravel, be careful with raw queries:</p>
<pre><code class="language-php">// ❌ Still vulnerable
DB::select("SELECT * FROM users WHERE id = " . $userId);

// ✅ Use bindings
DB::select("SELECT * FROM users WHERE id = ?", [$userId]);
</code></pre>
<h2>Best Practices</h2>
<ol>
<li><strong>Always use parameterized queries</strong></li>
<li><strong>Never concatenate user input into SQL</strong></li>
<li><strong>Use ORM/Query builders when possible</strong></li>
<li><strong>Validate and sanitize input</strong></li>
<li><strong>Apply principle of least privilege to database users</strong></li>
</ol>
<h2>Real-World Impact</h2>
<blockquote>
<p>In 2023, SQL injection attacks accounted for 30% of all web application breaches.</p>
</blockquote>
<p>Don't be a statistic - protect your applications!</p>]]></content>
            </entry>
        <entry>
        <title>Docker Multi-Stage Builds for Smaller Images</title>
        <link href="https://cslant.com/tips/docker-multi-stage-builds" />
        <id>https://cslant.com/tips/docker-multi-stage-builds</id>
        <updated>2026-06-22T14:41:52+07:00</updated>
                        <category term="DevOps" />
                        <summary>Reduce your Docker image size dramatically using multi-stage builds.</summary>
                        <content type="html"><![CDATA[<h1>Docker Multi-Stage Builds</h1>
<p>Multi-stage builds allow you to create smaller, more secure Docker images by separating build and runtime environments.</p>
<h2>The Problem</h2>
<p>Traditional Dockerfile includes build tools in the final image:</p>
<pre><code class="language-dockerfile">FROM node:18
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
CMD ["node", "dist/index.js"]
</code></pre>
<p>Result: ~1GB image with unnecessary build dependencies!</p>
<h2>The Solution: Multi-Stage Build</h2>
<pre><code class="language-dockerfile"># Build stage
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Production stage
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/index.js"]
</code></pre>
<p>Result: ~200MB image with only runtime dependencies!</p>
<h2>Benefits</h2>
<ul>
<li><strong>Smaller images</strong>: 5-10x size reduction</li>
<li><strong>Faster deployments</strong>: Less data to transfer</li>
<li><strong>Better security</strong>: Fewer attack surfaces</li>
<li><strong>Cleaner production</strong>: No build tools in production</li>
</ul>
<h2>For Laravel Applications</h2>
<pre><code class="language-dockerfile"># Composer dependencies
FROM composer:2 AS composer
WORKDIR /app
COPY composer.* ./
RUN composer install --no-dev --optimize-autoloader

# Node assets
FROM node:18 AS node
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Production
FROM php:8.3-fpm-alpine
WORKDIR /var/www
COPY --from=composer /app/vendor ./vendor
COPY --from=node /app/public/build ./public/build
COPY . .
</code></pre>
<h2>Flow Diagram</h2>
<div class="page-mermaid"><div class="mermaid">graph LR
    A[Build Stage] --&gt; B[Install Dependencies]
    B --&gt; C[Build Application]
    C --&gt; D[Production Stage]
    D --&gt; E[Copy Only Required Files]
    E --&gt; F[Small Final Image]</div></div>
<blockquote>
<p>Multi-stage builds are a Docker best practice that every developer should use!</p>
</blockquote>]]></content>
            </entry>
        <entry>
        <title>PHP 8.4 Property Hooks Explained</title>
        <link href="https://cslant.com/tips/php-8-4-property-hooks" />
        <id>https://cslant.com/tips/php-8-4-property-hooks</id>
        <updated>2026-06-21T14:41:52+07:00</updated>
                        <category term="PHP" />
                        <summary>Discover the new property hooks feature in PHP 8.4 that provides a cleaner way to add logic to property access.</summary>
                        <content type="html"><![CDATA[<h1>PHP 8.4 Property Hooks</h1>
<p>PHP 8.4 introduces <strong>property hooks</strong>, a powerful feature that allows you to add custom logic when getting or setting properties.</p>
<h2>Basic Example</h2>
<pre><code class="language-php">class User
{
    public string $name {
        get =&gt; strtoupper($this-&gt;name);
        set =&gt; ucfirst($value);
    }
}

$user = new User();
$user-&gt;name = 'john doe';
echo $user-&gt;name; // Output: JOHN DOE
</code></pre>
<h2>Computed Properties</h2>
<p>Create virtual properties without backing storage:</p>
<pre><code class="language-php">class Rectangle
{
    public function __construct(
        public float $width,
        public float $height
    ) {}

    public float $area {
        get =&gt; $this-&gt;width * $this-&gt;height;
    }
}
</code></pre>
<h2>Validation in Setters</h2>
<pre><code class="language-php">class Product
{
    private float $_price;

    public float $price {
        get =&gt; $this-&gt;_price;
        set {
            if ($value &lt; 0) {
                throw new InvalidArgumentException('Price must be positive');
            }
            $this-&gt;_price = $value;
        }
    }
}
</code></pre>
<h2>Benefits</h2>
<ul>
<li>✅ Cleaner than <code>__get()</code> and <code>__set()</code> magic methods</li>
<li>✅ Better IDE support and static analysis</li>
<li>✅ More explicit and readable code</li>
<li>✅ Type-safe property access</li>
</ul>
<blockquote>
<p>Property hooks make PHP objects more powerful while maintaining backward compatibility!</p>
</blockquote>]]></content>
            </entry>
        <entry>
        <title>Laravel Query Optimization with Eager Loading</title>
        <link href="https://cslant.com/tips/laravel-query-optimization-eager-loading" />
        <id>https://cslant.com/tips/laravel-query-optimization-eager-loading</id>
        <updated>2026-06-19T14:41:52+07:00</updated>
                        <category term="Laravel" />
                        <summary>Learn how to optimize database queries in Laravel by using eager loading to prevent N+1 query problems.</summary>
                        <content type="html"><![CDATA[<h1>Laravel Query Optimization with Eager Loading</h1>
<p>When working with Laravel's Eloquent ORM, one of the most common performance killers is the <strong>N+1 query problem</strong>. It appears the moment you load a collection of models and then touch a relationship inside a loop.</p>
<h2>The N+1 problem</h2>
<pre><code class="language-php">$posts = Post::all();            // 1 query

foreach ($posts as $post) {
    echo $post-&gt;author-&gt;name;    // +1 query PER post
}
</code></pre>
<p>Load 50 posts and you fire <strong>51 queries</strong>: one for the posts, plus one for every author. It is invisible in code review but deadly under load.</p>
<div class="page-mermaid"><div class="mermaid">flowchart TD
    A[Post all - 1 query] --&gt; B{Loop each post}
    B --&gt; C[post.author - query 2]
    B --&gt; D[post.author - query 3]
    B --&gt; E[post.author - query 51]
    style C fill:#fee2e2,stroke:#ef4444
    style D fill:#fee2e2,stroke:#ef4444
    style E fill:#fee2e2,stroke:#ef4444</div></div>
<h2>The fix: eager loading with with()</h2>
<pre><code class="language-php">$posts = Post::with('author')-&gt;get();   // 2 queries total

foreach ($posts as $post) {
    echo $post-&gt;author-&gt;name;            // no extra queries
}
</code></pre>
<p>Laravel runs one query for posts and a <strong>single</strong> <code>WHERE author_id IN (...)</code> for all authors - 2 queries no matter how many posts.</p>
<h2>Nested &amp; multiple relationships</h2>
<pre><code class="language-php">// Multiple relations
$posts = Post::with(['author', 'comments'])-&gt;get();

// Nested relations (dot notation)
$posts = Post::with('comments.user')-&gt;get();

// Constrain a relation with a closure
$posts = Post::with(['comments' =&gt; fn ($q) =&gt; $q-&gt;latest()-&gt;limit(5)])-&gt;get();
</code></pre>
<h2>Load only the columns you need</h2>
<pre><code class="language-php">$posts = Post::with('author:id,name')-&gt;get();
</code></pre>
<p>Selecting <code>id,name</code> keeps the payload small - just always include the foreign key (<code>id</code>) or the relation will not match.</p>
<h2>Eager load after the fact</h2>
<p>Already have the collection? Load relationships afterwards with <code>load()</code>:</p>
<pre><code class="language-php">$posts = Post::all();
$posts-&gt;load('author');
</code></pre>
<h2>Catch N+1 automatically</h2>
<p>Enable strict mode in a service provider so lazy loading throws during development:</p>
<pre><code class="language-php">use Illuminate\Database\Eloquent\Model;

Model::preventLazyLoading(! app()-&gt;isProduction());
</code></pre>
<h2>Key takeaways</h2>
<ul>
<li><code>with()</code> eager-loads upfront (2 queries); <code>load()</code> eager-loads an existing collection.</li>
<li>Always eager-load any relationship you touch inside a loop.</li>
<li>Use column selection and closures to load only what you need.</li>
<li>Turn on <code>preventLazyLoading</code> in dev to catch N+1 before it ships.</li>
</ul>]]></content>
            </entry>
        <entry>
        <title>JavaScript Async/Await Best Practices</title>
        <link href="https://cslant.com/tips/javascript-async-await-best-practices" />
        <id>https://cslant.com/tips/javascript-async-await-best-practices</id>
        <updated>2026-06-17T14:41:52+07:00</updated>
                        <category term="JavaScript" />
                        <summary>Master async/await in JavaScript with these best practices and common pitfalls to avoid.</summary>
                        <content type="html"><![CDATA[<h1>JavaScript Async/Await Best Practices</h1>
<p>Modern JavaScript uses <code>async/await</code> for handling asynchronous operations. Here are the best practices:</p>
<h2>1. Always Handle Errors</h2>
<pre><code class="language-javascript">async function fetchUserData(userId) {
    try {
        const response = await fetch(`/api/users/${userId}`);
        const data = await response.json();
        return data;
    } catch (error) {
        console.error('Failed to fetch user:', error);
        throw error;
    }
}
</code></pre>
<h2>2. Parallel Execution</h2>
<p>Don't await in sequence when you can run in parallel:</p>
<pre><code class="language-javascript">// ❌ Slow - Sequential
const user = await fetchUser();
const posts = await fetchPosts();
const comments = await fetchComments();

// ✅ Fast - Parallel
const [user, posts, comments] = await Promise.all([
    fetchUser(),
    fetchPosts(),
    fetchComments()
]);
</code></pre>
<h2>3. Promise.allSettled for Independent Operations</h2>
<pre><code class="language-javascript">const results = await Promise.allSettled([
    fetchUser(),
    fetchPosts(),
    fetchComments()
]);

results.forEach(result =&gt; {
    if (result.status === 'fulfilled') {
        console.log('Success:', result.value);
    } else {
        console.log('Error:', result.reason);
    }
});
</code></pre>
<h2>4. Avoid Async in Loops</h2>
<pre><code class="language-javascript">// ❌ Bad
for (const id of userIds) {
    await processUser(id);
}

// ✅ Good
await Promise.all(userIds.map(id =&gt; processUser(id)));
</code></pre>
<h2>Key Takeaways</h2>
<ul>
<li>Always use try/catch for error handling</li>
<li>Use Promise.all() for parallel operations</li>
<li>Consider Promise.allSettled() for independent tasks</li>
<li>Avoid awaiting inside loops</li>
</ul>
<blockquote>
<p>Async/await makes asynchronous code look synchronous, but understanding Promises is still crucial!</p>
</blockquote>]]></content>
            </entry>
    </feed>
