<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[TechKis]]></title><description><![CDATA[TechKis]]></description><link>https://blogs.techkis.tech</link><image><url>https://cdn.hashnode.com/uploads/logos/6a166d51da253d50d4118b39/02b20a89-10f4-48db-b035-9c28090a280a.png</url><title>TechKis</title><link>https://blogs.techkis.tech</link></image><generator>RSS for Node</generator><lastBuildDate>Tue, 13 Oct 2026 00:37:24 GMT</lastBuildDate><atom:link href="https://blogs.techkis.tech/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[JWT vs Sessions — The Part Nobody Tells You Until It's in Production]]></title><description><![CDATA[Almost every auth tutorial I've read in the last five years reaches for JWT. It's the default now. And the reasons given are always the same three: it's stateless, it scales, and you don't hit the dat]]></description><link>https://blogs.techkis.tech/jwt-vs-sessions-what-nobody-tells-you</link><guid isPermaLink="true">https://blogs.techkis.tech/jwt-vs-sessions-what-nobody-tells-you</guid><category><![CDATA[authentication]]></category><category><![CDATA[Security]]></category><category><![CDATA[backend]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[api]]></category><category><![CDATA[APIs]]></category><dc:creator><![CDATA[TechKis]]></dc:creator><pubDate>Wed, 22 Jul 2026 05:39:59 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a166d51da253d50d4118b39/da63b9db-269b-4745-a9f1-f63acc2294eb.svg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Almost every auth tutorial I've read in the last five years reaches for JWT. It's the default now. And the reasons given are always the same three: it's stateless, it scales, and you don't hit the database on every request.</p>
<p>All three of those are true. I want to be clear about that up front, because this post is not a "JWT is insecure" post. JWT is a perfectly good, well-specified format, and the cryptography is fine.</p>
<p>The problem is something else entirely, and it usually doesn't surface until you're live: <strong>you can't take a JWT back.</strong></p>
<h2>What "stateless" actually means</h2>
<p>A JWT carries its own claims. Your server signs a payload, hands it to the client, and from then on verification is a local operation — check the signature, check <code>exp</code>, trust the contents. No lookup, no shared store, no round trip.</p>
<p>That's genuinely elegant. It's also the exact property that causes the trouble, because "no lookup" and "I can change my mind about this token" are mutually exclusive. If the server never asks anything about the token, it has no place to record that it changed its mind.</p>
<p>So here's the sentence I wish tutorials led with: <strong>a signed JWT is valid until it expires, and nothing you do in between can stop that.</strong></p>
<h2>The concrete version</h2>
<p>Say you issue a one-hour access token at 09:00. At 09:05 you decide that user is abusive and ban the account — flip <code>is_active</code> to false, feel good about it.</p>
<p>At 09:06 that same token still returns <code>200 OK</code> on your protected endpoints. So does the one at 09:30. The token doesn't know anything happened, and neither does your verification code, because your verification code doesn't ask.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a166d51da253d50d4118b39/a3c2441f-5800-4479-bef2-314e31415313.svg" alt="A timeline showing a JWT issued at 09:00 with a decoded payload containing role admin, the account being revoked on the server at 09:05, and the same unchanged token still returning 200 OK at 09:06 because it stays valid until its expiry" style="display:block;margin:0 auto" />

<p>Three flavours of the same bug, all of which I've seen discussed by teams who were surprised by them:</p>
<ul>
<li><p><strong>Logout doesn't log out.</strong> Clearing the token client-side is the whole logout implementation in most JWT tutorials. If a copy of that token exists anywhere else — a saved request, a second device, a proxy log — it still works.</p>
</li>
<li><p><strong>Bans have a delay.</strong> Whatever your access-token lifetime is, that's your worst-case window for an abusive account to keep operating.</p>
</li>
<li><p><strong>Permission downgrades don't take effect.</strong> You demote someone from <code>admin</code> to <code>viewer</code>. Their existing token still says <code>"role": "admin"</code>, and if your middleware trusts the claim (which is the entire point of putting it there), they're still an admin until expiry.</p>
</li>
</ul>
<p>That last one is the sharpest, because it's not a security bug in JWT — it's a design consequence. Claims are a snapshot of the moment the token was signed. Anything that can change after signing shouldn't be trusted from the token alone.</p>
<h2>Every fix reintroduces the state</h2>
<p>This is the part I find genuinely interesting. Each standard workaround is reasonable in isolation, and each one gives back the thing JWT was chosen for.</p>
<p><strong>Short expiry plus refresh tokens.</strong> Cut access tokens to 5–15 minutes and issue a long-lived refresh token. Revocation now takes effect within one access-token lifetime. But refresh tokens must be revocable to be worth anything, which means they live in a database and get looked up on refresh. You now have server-side session state — you've just renamed it and added a rotation protocol on top.</p>
<p><strong>A denylist of revoked tokens.</strong> Store the <code>jti</code> of every revoked token and check it on each request. This works and it's correct. It's also a store lookup on every single request, which is exactly what sessions do, except your lookup key is bigger and you're carrying signature verification as well.</p>
<p><strong>A token version column.</strong> My favourite of the three, and still the same trade:</p>
<pre><code class="language-js">// user row: { id: 42, token_version: 3 }
// JWT payload: { sub: 42, tv: 3, role: "viewer", exp: ... }

async function verify(token) {
  const claims = jwt.verify(token, PUBLIC_KEY); // signature + exp
  const user = await db.users.findById(claims.sub); // ← the lookup is back
  if (!user || user.token_version !== claims.tv) {
    throw new Error("token revoked");
  }
  return user;
}
</code></pre>
<p>Bump <code>token_version</code> and every outstanding token for that user dies immediately. It's clean, it's easy to reason about, and I'd happily ship it. But look at the second line of that function: you are querying the database on every request to validate a token whose selling point was not querying the database on every request.</p>
<p>That's the honest summary. <strong>There is no way to make a JWT revocable without adding server-side state, because revocation <em>is</em> server-side state.</strong> You can move where it lives and how often you check it, but you can't make it disappear.</p>
<h2>What a session actually costs in 2026</h2>
<p>The mental model a lot of people carry is that sessions mean a heavy database join on every request. They don't.</p>
<p>A session lookup is a single point read by primary key against a store you've already got — Redis, or a Postgres table with an index on <code>session_id</code>. That's a sub-millisecond class of operation on a warm local Redis, and it's on the order of a millisecond or two against Postgres in the same region. I'm not going to quote you a benchmark I didn't run, but the shape is: it's one indexed key lookup, and it's almost never what's slow about your endpoint.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a166d51da253d50d4118b39/9c9c8d67-ca1a-42ca-be47-13692eb41136.svg" alt="A timeline showing a session id in an httpOnly cookie, the server deleting the session row at 09:05, and the very next request at 09:06 returning 401 immediately because the single indexed lookup by session id finds nothing" style="display:block;margin:0 auto" />

<p>Compare that to the work you're doing anyway. Your handler probably fetches the user record to render anything useful, and your ORM probably issues three more queries you haven't looked at recently. Against that backdrop, the session read is noise.</p>
<p>"Stateless scales better" is a real argument. It is also, for the overwhelming majority of applications, a premature one. If you're running a handful of app servers behind a load balancer with one Redis, statelessness is buying you very little and costing you revocation.</p>
<table>
<thead>
<tr>
<th></th>
<th>Session cookie</th>
<th>JWT</th>
</tr>
</thead>
<tbody><tr>
<td>Revoke a single login</td>
<td>Delete one row — instant</td>
<td>Not possible without added state</td>
</tr>
<tr>
<td>Change a user's role</td>
<td>Next request sees it</td>
<td>Old token keeps old claims until <code>exp</code></td>
</tr>
<tr>
<td>Cost per request</td>
<td>One indexed key lookup</td>
<td>Signature verification, no I/O</td>
</tr>
<tr>
<td>Horizontal scaling</td>
<td>Needs a shared store</td>
<td>Nothing shared</td>
</tr>
<tr>
<td>Verifiable by another service</td>
<td>Only by calling you</td>
<td>Yes, with the public key</td>
</tr>
<tr>
<td>Failure mode</td>
<td>Store goes down, logins fail</td>
<td>Leaked token works until expiry</td>
</tr>
</tbody></table>
<p>Note the last row, because it cuts both ways. A shared session store is a dependency and it can go down. That's a real cost of sessions and I don't want to hand-wave it.</p>
<h2>Where you put the token matters more than which token it is</h2>
<p>Brief and precise, because this part attracts a lot of confident wrong advice.</p>
<p>For a browser app, put the credential in a cookie with all three flags set:</p>
<pre><code class="language-plaintext">Set-Cookie: sid=&lt;opaque-random-value&gt;;
  HttpOnly;
  Secure;
  SameSite=Lax;
  Path=/;
  Max-Age=1209600
</code></pre>
<p><code>HttpOnly</code> keeps JavaScript from reading it, which means an XSS payload can't simply exfiltrate it. <code>Secure</code> keeps it off plaintext connections. <code>SameSite=Lax</code> blocks it from most cross-site requests, which handles the common CSRF cases — though if you accept state-changing <code>GET</code>s or need <code>SameSite=None</code> for a cross-origin frontend, you still want explicit CSRF tokens.</p>
<p>Putting a JWT in <code>localStorage</code> gives up <code>HttpOnly</code> entirely. Any script that runs on your page — yours, a dependency's, an injected one — can read it and send it anywhere.</p>
<p>The standard reply is "but XSS is the real bug." That's true, and it's also not a defence — defence in depth means assuming one of your controls fails. That said, <code>HttpOnly</code> doesn't make you XSS-proof either: an attacker with script execution can still make authenticated requests as the user. What it stops is them walking away with a portable credential that keeps working after you've cleaned up. Meaningful, not complete.</p>
<p>Worth saying: a JWT <em>can</em> live in an <code>httpOnly</code> cookie. That's a fine setup. You just get the storage benefit without solving revocation.</p>
<h2>Where JWT is genuinely the right tool</h2>
<p>I don't want this to read as anti-JWT, because there are cases where it's clearly correct — and they all share one shape: <strong>the verifier can't or shouldn't call you.</strong></p>
<ul>
<li><p><strong>Service-to-service tokens.</strong> Short-lived, narrowly scoped, minted for a specific call. Nobody needs to revoke a token that lives for 90 seconds.</p>
</li>
<li><p><strong>Third-party API access.</strong> A partner validating your token with a published public key, with no path to your session store.</p>
</li>
<li><p><strong>Federated identity and OIDC ID tokens.</strong> This is what the spec was designed for. An identity provider asserts something, a relying party verifies the signature. The whole model depends on offline verification.</p>
</li>
<li><p><strong>Signed single-use links.</strong> Password reset, email verification, signed download URLs. Here short expiry isn't a workaround — it's the entire security property you want.</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/6a166d51da253d50d4118b39/1d7de78e-d307-46f7-9ae9-df5052d1666f.svg" alt="A two-column decision panel listing concrete situations for using a session cookie, such as first-party web app login and admin role changes, against situations for using a JWT, such as service-to-service calls and password reset links" style="display:block;margin:0 auto" />

<h2>The middle ground most teams land on</h2>
<p>After all that, the setup I'd reach for by default isn't exotic:</p>
<p><strong>Opaque session token in an</strong> <code>httpOnly</code> <strong>cookie for your own first-party web and mobile clients.</strong> Revocation is a delete. Logout works. Role changes apply immediately. One indexed lookup per request.</p>
<p><strong>Short-lived JWTs only at the boundary where another service has to verify without calling you.</strong> Minutes, not days, and scoped to what that service actually needs.</p>
<p>That's roughly what a well-configured Laravel Sanctum setup gives you, incidentally — opaque database-backed tokens for your own app, which is why we used it on Puratan Ayurveda rather than reaching for JWT out of habit.</p>
<h2>My personal take</h2>
<p>I think JWT got popular for a reason that has almost nothing to do with its actual strengths: it <em>felt</em> like the modern choice, and sessions felt like the thing PHP did in 2009.</p>
<p>But statelessness isn't free. You pay for it in revocation, and revocation is not an edge case — it's logout, bans, role changes, and every incident response where step one is "cut off that session." Those are core product features, not exotic requirements.</p>
<p>So my rule is simple. If the thing verifying the token can call your server, use a session. If it can't, use a JWT, keep it short-lived, and be honest that anyone holding a copy is authenticated until it expires. Pick based on who's doing the verifying — not on which one sounds more scalable.</p>
<h2>The short version</h2>
<ul>
<li><p>A signed JWT is valid until <code>exp</code>. Logout, bans, and role changes don't affect an already-issued token.</p>
</li>
<li><p>Every revocation fix — refresh tokens, denylists, token-version columns — adds back server-side state.</p>
</li>
<li><p>A session lookup is one indexed key read in Redis or Postgres. It's rarely your bottleneck.</p>
</li>
<li><p>Use <code>HttpOnly</code> + <code>Secure</code> + <code>SameSite</code> cookies. <code>localStorage</code> exposes the token to any XSS on your page.</p>
</li>
<li><p>JWT is the right call for service-to-service calls, third-party verification, OIDC, and signed one-off links.</p>
</li>
<li><p>Sensible default: opaque session cookie for your own app, short-lived JWT only where another service must verify offline.</p>
</li>
</ul>
<hr />
<p><em>Written by the TechKis team — an AI-first engineering studio.</em> <a href="https://techkis.tech"><em>techkis.tech</em></a></p>
]]></content:encoded></item><item><title><![CDATA[You Don't Need Redis, Elasticsearch and Kafka Yet — Postgres Does All Three]]></title><description><![CDATA[There's a diagram I keep seeing. A team with maybe 2,000 users sketches their architecture, and it has six datastores in it. Postgres for the real data. Redis for caching. Elasticsearch for search. Ka]]></description><link>https://blogs.techkis.tech/you-don-t-need-redis-elasticsearch-and-kafka-yet-postgres-does-all-three</link><guid isPermaLink="true">https://blogs.techkis.tech/you-don-t-need-redis-elasticsearch-and-kafka-yet-postgres-does-all-three</guid><category><![CDATA[PostgreSQL]]></category><category><![CDATA[Databases]]></category><category><![CDATA[System Design]]></category><category><![CDATA[backend]]></category><category><![CDATA[architecture]]></category><dc:creator><![CDATA[TechKis]]></dc:creator><pubDate>Wed, 22 Jul 2026 05:32:03 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a166d51da253d50d4118b39/db33de79-faef-4231-902b-8ef020a3bcca.svg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>There's a diagram I keep seeing. A team with maybe 2,000 users sketches their architecture, and it has six datastores in it. Postgres for the real data. Redis for caching. Elasticsearch for search. Kafka for events. Mongo for the "flexible" documents. Something time-series for the dashboard.</p>
<p>It looks impressive. It looks like the architecture diagrams in the conference talks. And it is, for a team that size, mostly a liability.</p>
<p>Here's the thing nobody says out loud when that diagram gets drawn: every box on it is a deployment. It's also a backup policy, a restore procedure you have hopefully tested, a monitoring dashboard, a set of alert thresholds, a credential to rotate, an upgrade path, a failure mode at 3am, and — the expensive one — a thing that can silently disagree with the other five boxes.</p>
<p>I want to make the case that Postgres already does a credible version of most of that. Not a <em>better</em> version. A credible one, which for a small system is the correct trade.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a166d51da253d50d4118b39/318daf4c-3242-47aa-8133-07234cf4ecac.svg" alt="On the left, an architecture diagram a small team drew with six datastores — App, Postgres, Redis, Elasticsearch, Kafka and Mongo — tallying six deployments, six backup policies, six dashboards and six on-call runbooks; on the right, what two thousand users actually need, a single app talking to a single Postgres instance with a tally of one" style="display:block;margin:0 auto" />

<h2>Caching: an UNLOGGED TABLE is genuinely fast</h2>
<p>Most applications that say "we need caching" mean: there are a few thousand rows that get read constantly and change rarely, and recomputing them is annoying.</p>
<p>Postgres has a purpose-built thing for exactly this. An unlogged table skips the write-ahead log entirely. That makes writes dramatically cheaper, at the cost of the table being truncated if the server crashes — which for a cache is not a bug, it's the definition.</p>
<pre><code class="language-sql">CREATE UNLOGGED TABLE cache_entries (
    key         text PRIMARY KEY,
    value       jsonb NOT NULL,
    expires_at  timestamptz NOT NULL
);

CREATE INDEX ON cache_entries (expires_at);

-- write / overwrite
INSERT INTO cache_entries (key, value, expires_at)
VALUES ('dashboard:org:42', '{"revenue": 91200}'::jsonb, now() + interval '5 minutes')
ON CONFLICT (key) DO UPDATE
    SET value = EXCLUDED.value,
        expires_at = EXCLUDED.expires_at;

-- read
SELECT value
FROM cache_entries
WHERE key = 'dashboard:org:42'
  AND expires_at &gt; now();
</code></pre>
<p>That's a primary-key lookup on a table that's almost certainly fully in memory. It is fast in a way that will surprise you if you've only ever thought of Postgres as "the slow durable thing".</p>
<p>Being fair to Redis: it wins decisively on raw operations per second, on the data structures Postgres doesn't have (sorted sets, HyperLogLog), on pub/sub, and on atomic counters under heavy contention. Those are real advantages. They are just not what most teams mean when they say the word "caching".</p>
<h2>Full-text search: tsvector plus a GIN index</h2>
<p>This is the one people underestimate most. Postgres full-text search is not a toy. You get stemming, stop words, ranking, prefix matching and multi-column weighting, all in the query planner you already understand.</p>
<pre><code class="language-sql">ALTER TABLE articles
ADD COLUMN search_vector tsvector
GENERATED ALWAYS AS (
    setweight(to_tsvector('english', coalesce(title, '')), 'A') ||
    setweight(to_tsvector('english', coalesce(body,  '')), 'B')
) STORED;

CREATE INDEX articles_search_idx ON articles USING GIN (search_vector);
</code></pre>
<p>The generated column keeps itself in sync on every insert and update — no trigger to write, no drift, no reindex job. Then the query:</p>
<pre><code class="language-sql">SELECT id,
       title,
       ts_rank(search_vector, query) AS rank
FROM articles, websearch_to_tsquery('english', 'postgres queue locking') AS query
WHERE search_vector @@ query
ORDER BY rank DESC
LIMIT 20;
</code></pre>
<p><code>websearch_to_tsquery</code> is the one to reach for, because it parses the syntax users actually type — quoted phrases, <code>or</code>, a leading minus to exclude. And <code>ts_rank</code> gives you real relevance ordering, weighted by the <code>setweight</code> calls above so a title match beats a body match.</p>
<p>My honest read on the ceiling: this holds up well into the millions of rows on ordinary hardware. I'm deliberately saying "well into the millions" rather than quoting a number, because the real limit depends on your document sizes, your update rate and how many filters you stack on top. Test it against your own data rather than trusting anyone's headline figure, mine included.</p>
<p>Elasticsearch earns its keep when relevance tuning becomes an ongoing product activity — synonyms, custom analyzers, per-language stemming, faceted navigation across large result sets, typo tolerance you actively tune. If someone on your team is going to spend real time on search quality, you want the tool built for that. If search is a box in the header that needs to find the right thing, Postgres is fine.</p>
<h2>Queues: FOR UPDATE SKIP LOCKED is a real pattern</h2>
<p><code>SKIP LOCKED</code> arrived in Postgres 9.5 and it quietly solved the "you can't build a queue on a database" objection. It lets a worker claim rows while stepping over rows another worker has already locked, with no contention and no lost jobs.</p>
<pre><code class="language-sql">CREATE TABLE jobs (
    id           bigserial PRIMARY KEY,
    payload      jsonb NOT NULL,
    status       text NOT NULL DEFAULT 'pending',
    run_after    timestamptz NOT NULL DEFAULT now(),
    attempts     int NOT NULL DEFAULT 0,
    locked_at    timestamptz
);

CREATE INDEX jobs_claim_idx ON jobs (run_after)
WHERE status = 'pending';
</code></pre>
<p>And the claim query — this is the whole trick:</p>
<pre><code class="language-sql">UPDATE jobs
SET status = 'running',
    locked_at = now(),
    attempts = attempts + 1
WHERE id IN (
    SELECT id
    FROM jobs
    WHERE status = 'pending'
      AND run_after &lt;= now()
    ORDER BY run_after
    FOR UPDATE SKIP LOCKED
    LIMIT 10
)
RETURNING id, payload;
</code></pre>
<p>Run twenty workers against that and each one gets a disjoint batch. No double processing, no polling storm, no separate broker. And because it's the same transaction as your business data, you get something an external broker cannot give you for free: enqueue a job and write the row it depends on atomically. Either both happen or neither does.</p>
<p>That property is worth more than most teams realise. A huge share of "the job ran before the record was committed" bugs simply cannot occur in this design.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a166d51da253d50d4118b39/336e0494-dd57-4a87-9253-b9f83bed1bef.svg" alt="Three stacked editor panels showing real SQL: an UNLOGGED TABLE cache with an upsert replacing Redis, a generated tsvector column with a GIN index and a ts_rank query replacing Elasticsearch, and a FOR UPDATE SKIP LOCKED job claim query replacing a message broker" style="display:block;margin:0 auto" />

<h2>JSON documents and analytics, briefly</h2>
<p>Two more that follow the same shape.</p>
<p><strong><code>jsonb</code> covers most "we need Mongo" cases.</strong> You get schemaless columns, containment operators, path queries and GIN indexing on the whole document:</p>
<pre><code class="language-sql">CREATE INDEX events_props_idx ON events USING GIN (properties jsonb_path_ops);

SELECT * FROM events WHERE properties @&gt; '{"plan": "pro", "source": "referral"}';
</code></pre>
<p>The thing you keep, that a separate document store takes away, is joining that document to your relational data in one query.</p>
<p><strong>Time series and analytics</strong> are further than people expect too. Window functions handle running totals and rankings. <code>generate_series</code> fills gaps so your chart doesn't skip empty days. Materialized views precompute the expensive rollups on a schedule. That combination carries a dashboard a very long way before a specialist columnar store starts paying for itself.</p>
<h2>The real cost isn't the license</h2>
<p>Every one of these tools is free to download. That's not where the money goes.</p>
<p>The cost is operational surface. Concretely, per additional datastore:</p>
<table>
<thead>
<tr>
<th>What you take on</th>
<th>Why it compounds</th>
</tr>
</thead>
<tbody><tr>
<td>A deployment and upgrade path</td>
<td>Version skew, breaking changes, migration windows</td>
</tr>
<tr>
<td>A backup and restore procedure</td>
<td>Untested restores are not backups</td>
</tr>
<tr>
<td>Monitoring and alert thresholds</td>
<td>Another dashboard nobody reads until it's on fire</td>
</tr>
<tr>
<td>An on-call runbook</td>
<td>Someone has to know how this one fails</td>
</tr>
<tr>
<td>Credentials and network policy</td>
<td>Another thing to rotate and another exposed port</td>
</tr>
<tr>
<td><strong>Consistency with the other stores</strong></td>
<td><strong>This is the one that actually hurts</strong></td>
</tr>
</tbody></table>
<p>That last row is the killer. With one store, your data is either right or wrong. With two, it can be right in one and stale in the other. With six, the number of pairs that can disagree grows quadratically — six stores means fifteen pairs. Every one of those pairs is a class of bug that only shows up in production, usually as "the search results show a product we deleted."</p>
<p>You don't debug those quickly. You debug them by reasoning about two systems at once, at night.</p>
<h2>When each tool genuinely earns its place</h2>
<p>I don't want to be the person telling you Postgres does everything. It doesn't. Here's my honest read on when to actually split, with a concrete signal for each.</p>
<p><strong>Redis</strong> earns it when you have very high-throughput ephemeral state — rate limiting, session counters, leaderboards, presence, pub/sub fan-out to many subscribers. <em>You'll know because</em> your cache writes have become a meaningful share of your Postgres write load, or because you're doing tens of thousands of counter increments a second and seeing lock contention on a single hot row. That's the signal. Not "we should probably cache things."</p>
<p><strong>Elasticsearch</strong> earns it when search relevance <em>is</em> the product. Marketplaces, job boards, documentation portals, anything where a bad ranking loses a sale. <em>You'll know because</em> someone is filing tickets about result quality specifically, you want synonyms and typo tolerance and faceted counts across millions of documents, or you're supporting several languages with different analyzers. When search becomes a thing you tune weekly rather than a feature you shipped, move.</p>
<p><strong>Kafka</strong> earns it when you need a durable, replayable event log consumed by several independent services at their own pace. <em>You'll know because</em> you want to add a new consumer and have it read the last thirty days of history from the beginning — that single requirement is the honest test.</p>
<p>And that Kafka one deserves emphasis, because it's the most commonly mis-picked tool of the three. Kafka is a log, not a queue. Many teams reach for it wanting "background jobs with retries" and end up hand-building consumer groups, offset management and dead-letter handling — reimplementing a job queue on top of something that was never a job queue. If what you want is "run this task later, retry on failure", <code>SKIP LOCKED</code> or a proper broker is the closer fit.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a166d51da253d50d4118b39/eb0c4ddc-3188-4651-b85e-bdaa9b823f08.svg" alt="A signals table listing the concrete symptom that means it is genuinely time to add Redis, Elasticsearch or Kafka, with the wrong reason people usually give alongside the real signal that should trigger the move" style="display:block;margin:0 auto" />

<h2>Starting on Postgres does not trap you</h2>
<p>This is the argument I find most convincing, and it's the one that gets skipped.</p>
<p>The fear is that starting simple means a painful rewrite later. In practice it's the opposite. Moving <em>one</em> workload out of Postgres is a bounded project with a clear definition of done. Move search to Elasticsearch: you already know your query shapes, your document structure and your relevance requirements, because you've been running them in production. You dual-write, you compare results, you cut over, you delete the old code. Two weeks, maybe four, and it ends.</p>
<p>Running six systems from day one is not a project. It's an ongoing tax, paid every sprint, by everyone, forever — and paid at your least experienced moment, when you have the least idea which of those systems you'll actually need.</p>
<p>You're also making the extraction <em>easier</em> by waiting. The team that migrates search after two years of running it in Postgres knows exactly what good relevance looks like for their data. The team that chose Elasticsearch on day one is guessing at analyzer configuration for a product that doesn't exist yet.</p>
<h2>My personal take</h2>
<p>I think the six-box diagram is mostly a status object. It signals seriousness. It looks like what real engineering is supposed to look like, and there's a quiet fear that a diagram with two boxes means you're not building anything real.</p>
<p>But the actual engineering skill on display in a two-box architecture is usually higher. Knowing that <code>SKIP LOCKED</code> exists, that unlogged tables are a real cache, that a generated <code>tsvector</code> column keeps itself in sync — that's more specific knowledge than knowing Redis is fast.</p>
<p>We're a small studio. The one product we've shipped end to end, an Ayurvedic e-commerce platform on Angular and Laravel, runs on a single relational database, and every time I've been tempted to add a second store the honest answer was that I wanted the diagram, not the capability.</p>
<p>So my rule is simple. Add the specialist store when Postgres has <em>actually</em> stopped being good enough at the thing, with a metric you can point to. Not when you imagine it might. The imagined version of that moment arrives about two years before the real one, and everything you build in between is paid for twice.</p>
<h2>The short version</h2>
<ul>
<li>Six datastores means six deployments, six backup policies, six runbooks — and fifteen pairs that can disagree.</li>
<li><code>UNLOGGED TABLE</code> is a genuinely fast cache; most apps' cache is a few thousand rows.</li>
<li><code>tsvector</code> + a GIN index gives real ranking and stemming, and holds up well into the millions of rows.</li>
<li><code>SELECT ... FOR UPDATE SKIP LOCKED</code> is a correct, battle-tested queue — and it's transactional with your business data.</li>
<li><code>jsonb</code> with GIN covers most "we need Mongo"; window functions and materialized views cover most early analytics.</li>
<li>Redis wins on throughput and pub/sub. Elasticsearch wins when relevance is the product. Kafka wins when you need a replayable log — which is not a job queue.</li>
<li>Extracting one workload later is a bounded project. Running six from day one is an unbounded tax.</li>
</ul>
<hr />
<p><em>Written by the TechKis team — an AI-first engineering studio. <a href="https://techkis.tech">techkis.tech</a></em></p>
]]></content:encoded></item><item><title><![CDATA[Your API Is Slow and It's Probably N+1 Queries]]></title><description><![CDATA[Someone reports that a page takes four seconds to load. You open the controller expecting to find something horrifying — a nested loop, a missing index, a synchronous HTTP call to a third-party API.
I]]></description><link>https://blogs.techkis.tech/n-plus-1-queries-slow-api</link><guid isPermaLink="true">https://blogs.techkis.tech/n-plus-1-queries-slow-api</guid><category><![CDATA[Laravel]]></category><category><![CDATA[backend]]></category><category><![CDATA[backend developments]]></category><category><![CDATA[Java]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[Databases]]></category><category><![CDATA[performance]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[TechKis]]></dc:creator><pubDate>Wed, 22 Jul 2026 05:25:26 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a166d51da253d50d4118b39/4758cb8c-7787-4392-a2ed-463d9e0b57a6.svg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Someone reports that a page takes four seconds to load. You open the controller expecting to find something horrifying — a nested loop, a missing index, a synchronous HTTP call to a third-party API.</p>
<p>Instead you find eight lines of perfectly ordinary code. It's clean. It's readable. You'd approve it in a review without a second thought.</p>
<p>That's the thing about N+1 queries. They don't look like bugs. They look like good code.</p>
<h2>What an N+1 actually is</h2>
<p>Say you have a <code>/posts</code> endpoint that returns the 50 most recent posts, each with its author's name.</p>
<pre><code class="language-php">public function index()
{
    $posts = Post::latest()-&gt;take(50)-&gt;get();

    return PostResource::collection($posts);
}
</code></pre>
<p>And the resource:</p>
<pre><code class="language-php">public function toArray($request)
{
    return [
        'id'     =&gt; $this-&gt;id,
        'title'  =&gt; $this-&gt;title,
        'author' =&gt; $this-&gt;author-&gt;name,   // ← here
    ];
}
</code></pre>
<p>That single line is the whole problem.</p>
<p><code>Post::latest()-&gt;take(50)-&gt;get()</code> is <strong>one query</strong>. It fetches 50 posts. Fine.</p>
<p>Then the resource runs once per post. Each time it hits <code>$this-&gt;author</code>, Eloquent notices the relation hasn't been loaded and quietly goes and fetches it. That's <strong>50 more queries</strong>, one at a time, each one a round trip to the database.</p>
<pre><code class="language-sql">select * from posts order by created_at desc limit 50
select * from users where id = 7 limit 1
select * from users where id = 3 limit 1
select * from users where id = 7 limit 1   -- yes, again
select * from users where id = 12 limit 1
... 46 more
</code></pre>
<p>1 + 50 = <strong>51 queries</strong> for a page that needs two. And notice it re-fetches user 7 twice, because each lazy load is independent — Eloquent has no idea it already has that row in memory.</p>
<p>That's the N+1: one query to get the list, then N queries to get something related for each item.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a166d51da253d50d4118b39/92f69d4e-752f-4ef8-9f17-f14c193b9602.svg" alt="A Laravel controller and API resource that look completely clean on the left, and on the right a query log showing one select on the posts table followed by fifty repeated selects on the users table, totalling fifty-one queries and 1.9 seconds" style="display:block;margin:0 auto" />

<h2>Why it survives every code review</h2>
<p>Here's what makes this bug different from most bugs: <strong>there is nothing wrong with the code.</strong></p>
<p><code>$post-&gt;author-&gt;name</code> is exactly how you're supposed to use an ORM. That's the whole selling point — relations feel like object properties. The abstraction is doing its job beautifully, right up until the moment it isn't.</p>
<p>There's no error. No warning. No lint rule fires. The tests pass, because your test fixture has three posts.</p>
<p>And that's the second half of it: <strong>N is small in development and large in production.</strong> You seed five records locally, the endpoint returns in 40ms, and you ship. Six months later there are 5,000 records, someone bumps the page size to 100, and the same code is making 100 sequential round trips per request. At 2ms per round trip that's 200ms of pure latency doing nothing but waiting.</p>
<p>The bug didn't appear. It was always there. Your data just grew into it.</p>
<p>Multiply by concurrent users and you get the classic symptom: the app is fine at 10 users and falls over at 200, and your database CPU graph looks like a wall.</p>
<h2>The fix: tell the ORM what you need up front</h2>
<p>Eager loading. You say in advance which relations you're going to touch, and the ORM fetches them all in one extra query using a <code>WHERE IN</code>.</p>
<pre><code class="language-php">$posts = Post::with('author')
    -&gt;latest()
    -&gt;take(50)
    -&gt;get();
</code></pre>
<p>Two queries. Total.</p>
<pre><code class="language-sql">select * from posts order by created_at desc limit 50
select * from users where id in (7, 3, 12, 41, 5, ...)
</code></pre>
<p>Same output, same resource code, 51 queries down to 2. You can load several relations at once:</p>
<pre><code class="language-php">Post::with(['author', 'comments', 'tags'])-&gt;get();
</code></pre>
<p>That's four queries — one for posts, one each for authors, comments and tags — regardless of whether you're returning 50 posts or 5,000.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a166d51da253d50d4118b39/355e18aa-eea0-4aa5-8d27-157373466d09.svg" alt="The same controller with a with author call added, and a query log showing exactly two queries: one select on posts and one select on users using a where id in clause, completing in forty milliseconds" style="display:block;margin:0 auto" />

<p>Every mature ORM has this. Different spelling, same idea:</p>
<table>
<thead>
<tr>
<th>Stack</th>
<th>Eager load</th>
<th>Nested</th>
</tr>
</thead>
<tbody><tr>
<td>Laravel / Eloquent</td>
<td><code>-&gt;with('author')</code></td>
<td><code>-&gt;with('comments.user')</code></td>
</tr>
<tr>
<td>Rails / ActiveRecord</td>
<td><code>.includes(:author)</code></td>
<td><code>.includes(comments: :user)</code></td>
</tr>
<tr>
<td>Prisma</td>
<td><code>include: { author: true }</code></td>
<td>nested <code>include</code></td>
</tr>
<tr>
<td>TypeORM</td>
<td><code>relations: ['author']</code></td>
<td><code>relations: ['comments.user']</code></td>
</tr>
<tr>
<td>Django</td>
<td><code>select_related</code> / <code>prefetch_related</code></td>
<td><code>prefetch_related('comments__user')</code></td>
</tr>
</tbody></table>
<p>If you want it enforced rather than remembered, Laravel lets you fail loudly the moment anything lazy-loads:</p>
<pre><code class="language-php">// AppServiceProvider::boot()
Model::preventLazyLoading(! app()-&gt;isProduction());
</code></pre>
<p>In dev and test that throws a <code>LazyLoadingViolationException</code> the instant you touch an unloaded relation. In production it stays off, so you get a slow page rather than a 500. I turn this on in every new project now — it turns an invisible performance bug into a loud test failure.</p>
<h2>The nested trap</h2>
<p>This is the one that got me the most times. You fix an N+1, feel good about it, and immediately create another one a level down.</p>
<pre><code class="language-php">$posts = Post::with('comments')-&gt;get();   // fixed the comments N+1

foreach ($posts as $post) {
    foreach ($post-&gt;comments as $comment) {
        echo $comment-&gt;user-&gt;name;        // ← new N+1, one per comment
    }
}
</code></pre>
<p><code>comments</code> is eager loaded. <code>comments.user</code> is not. So now instead of one query per post, you get one query per <em>comment</em> — which is usually a much bigger number than the one you started with. Congratulations, you made it worse.</p>
<p>The fix is dot notation:</p>
<pre><code class="language-php">Post::with('comments.user')-&gt;get();
</code></pre>
<p>Three queries: posts, comments for those posts, users for those comments. Rails: <code>includes(comments: :user)</code>.</p>
<p>The rule I use: <strong>every relation you touch anywhere in the render path needs to appear in the <code>with()</code>.</strong> Not just the ones in the controller — the ones in your resource classes, your Blade partials, your accessors. Especially accessors, because a computed attribute quietly touching a relation is the sneakiest version of this bug there is.</p>
<h2>Over-fetching: the trap on the other side</h2>
<p>Eager loading is not free, and "just add <code>with()</code> to everything" is its own bug.</p>
<p><code>with('comments')</code> on 50 posts that each have 400 comments pulls <strong>20,000 rows into PHP memory</strong> to render a page that shows a comment count. You traded 51 fast queries for 2 queries and an out-of-memory error. That's not a win.</p>
<p>Three things I reach for instead:</p>
<p><strong>If you only need a number, use a count.</strong> No rows loaded at all.</p>
<pre><code class="language-php">Post::withCount('comments')-&gt;get();
// then: $post-&gt;comments_count
</code></pre>
<p><strong>If you only need a few columns, ask for a few columns.</strong> Include the foreign key, or the relation silently breaks — Eloquent needs <code>user_id</code> on comments to match them back to posts.</p>
<pre><code class="language-php">Post::with([
    'author:id,name',
    'comments:id,post_id,body',
])-&gt;get();
</code></pre>
<p><strong>If you only need a slice, constrain the relation.</strong></p>
<pre><code class="language-php">Post::with(['comments' =&gt; fn ($q) =&gt; $q-&gt;latest()-&gt;limit(3)])-&gt;get();
</code></pre>
<p>And the honest fourth option: sometimes the right answer isn't eager loading at all, it's a <code>join</code> with an explicit <code>select</code>, or a dedicated read query that returns exactly the shape the endpoint needs. ORMs are excellent until you're fighting them, and a hand-written query for one hot endpoint is not a failure of engineering.</p>
<h2>How to actually catch these</h2>
<p>You will not spot N+1s by reading code. I've tried. The code looks fine — that's the entire premise of this post. You catch them by counting.</p>
<p><strong>Log queries in dev.</strong> Cheap and immediate:</p>
<pre><code class="language-php">DB::listen(fn ($q) =&gt; logger()-&gt;debug($q-&gt;sql, ['time' =&gt; $q-&gt;time]));
</code></pre>
<p>Fifty near-identical lines in your log is the signature. Once you've seen it, you recognise it instantly.</p>
<p><strong>Use the tools.</strong> Laravel Debugbar puts a live query count in the corner of every page. Rails has Bullet, which pops a warning telling you exactly which <code>includes</code> you forgot. Prisma logs queries with <code>log: ['query']</code>. Django Debug Toolbar does the same job. All of them make the invisible visible.</p>
<p><strong>Assert query counts in tests.</strong> This is the one that actually stops regressions, because it fails in CI rather than in production:</p>
<pre><code class="language-php">public function test_posts_index_stays_within_query_budget(): void
{
    Post::factory()-&gt;count(50)-&gt;create();

    $queries = 0;
    DB::listen(function () use (&amp;$queries) { $queries++; });

    $this-&gt;getJson('/api/posts')-&gt;assertOk();

    $this-&gt;assertLessThan(6, $queries);
}
</code></pre>
<p>Seed <strong>50</strong> records, not 3. A test with three fixtures passes happily at 4 queries whether the code is O(1) or O(N). The whole point is to make N big enough that a regression is obvious.</p>
<p>Then set a budget and hold the line: <em>no endpoint may exceed N queries.</em> Pick a number — 10 is a reasonable starting point for most read endpoints — and put it in CI. Budgets work because they're boring and automatic, and nobody has to remember anything.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a166d51da253d50d4118b39/857a9336-6c89-42ad-b7d8-8db82daa66d8.svg" alt="A PHPUnit test that seeds fifty posts, counts queries with a database listener and asserts the endpoint stays under six queries, next to a debugbar-style panel showing a query counter going from fifty-one queries in red to three queries in green" style="display:block;margin:0 auto" />

<h2>The honest caveat</h2>
<p>Not every query inside a loop is a bug.</p>
<p>If you're loading five configured payment providers, or the seven days of a week, or three tenant settings — N is bounded, small, and never going to grow. Five extra queries against an indexed primary key is a couple of milliseconds. Rewriting that into a clever eager-load makes the code harder to read and buys you nothing you can measure.</p>
<p>The failure mode I actually see more often in review isn't the missed N+1 — it's someone eager-loading six relations "to be safe" on an endpoint that uses two of them, then wondering why the response got slower.</p>
<p>Optimise the endpoints you can measure. Find the slow one, count its queries, fix that. Then go back to writing features.</p>
<h2>My personal take</h2>
<p>What I find interesting about N+1 is that it's not really a database problem. It's an abstraction problem.</p>
<p>ORMs sell you the idea that <code>$post-&gt;author</code> is just a property. It isn't — it's a network round trip wearing a property's clothes. Ninety-five percent of the time that illusion is exactly what you want, and it makes the code genuinely nicer to write and read. The other five percent, it hands you a 51-query endpoint that no reviewer will ever flag.</p>
<p>So I don't think the lesson is "be careful with ORMs." The lesson is that <strong>you can't reason about this by reading — you have to measure it.</strong> Query count is the single cheapest performance metric in a web app. It's one number, it's trivially available, and it correlates with slowness better than almost anything else you can check that fast.</p>
<p>We had a version of this on the Puratan Ayurveda build — a product listing that felt sluggish and turned out to be doing per-product lookups for related data. It wasn't clever to fix. It was one <code>with()</code> call. That's usually how this goes: the diagnosis is the hard part, the fix is one line.</p>
<p>Put a query counter in front of your face in dev, put a budget in CI, and this whole class of bug mostly stops happening to you.</p>
<h2>The short version</h2>
<ul>
<li>An N+1 is 1 query for the list plus N more for each item's relation. 50 posts + authors = 51 queries where 2 would do.</li>
<li>It passes review because the code is idiomatic and correct — it only hurts once N grows in production.</li>
<li>Fix it with eager loading: <code>with('author')</code> in Laravel, <code>includes</code> in Rails, <code>include</code> in Prisma, <code>relations</code> in TypeORM.</li>
<li>Watch the nested case: <code>with('comments')</code> then touching <code>$comment-&gt;user</code> just moves the N+1 down a level. Use <code>with('comments.user')</code>.</li>
<li>Don't over-correct. Use <code>withCount()</code> for numbers, <code>with('author:id,name')</code> for specific columns, constrained closures for slices.</li>
<li>Catch it by counting: <code>DB::listen</code> in dev, Debugbar or Bullet locally, query-count assertions in tests with 50 seeded records, and a hard budget in CI.</li>
<li>Turn on <code>Model::preventLazyLoading()</code> outside production so lazy loads fail loudly instead of silently.</li>
<li>If N is genuinely small and bounded, leave it alone. Optimise what you can measure.</li>
</ul>
<hr />
<p><em>Written by the TechKis team — an AI-first engineering studio. <a href="https://techkis.tech">techkis.tech</a></em></p>
]]></content:encoded></item><item><title><![CDATA[Your Docker Image Is 1.2GB. Here's the 180MB Version.]]></title><description><![CDATA[The first time I ran docker images on a Node API I'd just containerised, the number genuinely embarrassed me. 1.2GB. For an Express app that was maybe 400 lines of my own code.
I remember staring at i]]></description><link>https://blogs.techkis.tech/your-docker-image-is-1-2gb-here-s-the-180mb-version</link><guid isPermaLink="true">https://blogs.techkis.tech/your-docker-image-is-1-2gb-here-s-the-180mb-version</guid><category><![CDATA[Docker]]></category><category><![CDATA[Devops]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[backend]]></category><category><![CDATA[performance]]></category><dc:creator><![CDATA[TechKis]]></dc:creator><pubDate>Tue, 21 Jul 2026 16:27:05 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a166d51da253d50d4118b39/10c35332-9bf3-4477-b374-ca0b7404d6a3.svg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The first time I ran <code>docker images</code> on a Node API I'd just containerised, the number genuinely embarrassed me. <strong>1.2GB.</strong> For an Express app that was maybe 400 lines of my own code.</p>
<p>I remember staring at it thinking: what is in there? I didn't write 1.2GB of anything.</p>
<p>That question is worth answering, because the answer is almost always the same three things, and all three are fixable in about twenty minutes. This post is that walkthrough — the naive Dockerfile, why it's so big, and the version I'd actually ship.</p>
<h2>First, let's find out what's actually in there</h2>
<p>Here's the Dockerfile almost everyone writes first. It works. It's also the problem.</p>
<pre><code class="language-dockerfile">FROM node:20

WORKDIR /app
COPY . .
RUN npm install
RUN npm run build

CMD ["node", "dist/main.js"]
</code></pre>
<p>Nothing here is <em>wrong</em>, exactly. It builds, it runs, it ships. But four things are quietly inflating it:</p>
<ul>
<li><strong><code>node:20</code> is a full Debian image.</strong> Around 1.1GB before you add a single line of your own code. It ships a complete Linux userland — apt, Python, build toolchains, man pages. You are shipping a general-purpose operating system to run one JavaScript process.</li>
<li><strong><code>npm install</code> installs devDependencies.</strong> TypeScript, ESLint, Jest, your entire test suite, webpack or Vite, all of it. None of it runs in production. All of it ships.</li>
<li><strong><code>COPY . .</code> copies everything.</strong> Including <code>.git</code> — which on a busy repo can be hundreds of megabytes of history — plus your local <code>node_modules</code>, <code>.env</code> files, screenshots, logs, coverage reports.</li>
<li><strong>The build tooling stays in the final image.</strong> You needed TypeScript to produce <code>dist/</code>. You do not need TypeScript to <em>run</em> <code>dist/</code>. But it's still in there.</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/6a166d51da253d50d4118b39/8c15c4c8-c80e-466d-9644-a96067714520.svg" alt="A 1.2GB image broken down into layers: a 1.1GB full Debian base, devDependencies like TypeScript, ESLint and Jest, a copied .git directory and local node_modules, and only 2MB of actual application code" style="display:block;margin:0 auto" />

<p>That last bullet is the one worth sitting with. Your application code is often the smallest thing in your application image.</p>
<h2>Fix one: stop copying junk in</h2>
<p>This is the cheapest win and most people skip it. Create a <code>.dockerignore</code> next to your Dockerfile:</p>
<pre><code>node_modules
.git
.env*
dist
coverage
*.log
.vscode
README.md
</code></pre>
<p>Two reasons this matters more than it looks.</p>
<p>The obvious one: less stuff copied, smaller image.</p>
<p>The non-obvious one: <strong><code>COPY . .</code> invalidates your build cache every time any file changes.</strong> Edit the README, and Docker re-runs your entire <code>npm install</code>. Ignoring the noisy files means your cache survives longer, and your builds get dramatically faster — which you'll feel on every CI run, not just once.</p>
<p>Also, <code>.env</code> in a Docker image is a real security problem. Anyone who can pull that image can read your secrets with <code>docker history</code>. <code>.dockerignore</code> is the simplest guard against doing that by accident.</p>
<h2>Fix two: multi-stage builds</h2>
<p>This is the big one. The idea is simple: build in one image, run in a different, much smaller one, and copy only the finished output across.</p>
<pre><code class="language-dockerfile"># ---- build stage ----
FROM node:20-alpine AS builder
WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

# ---- runtime stage ----
FROM node:20-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production

COPY package*.json ./
RUN npm ci --omit=dev &amp;&amp; npm cache clean --force

COPY --from=builder /app/dist ./dist

USER node
CMD ["node", "dist/main.js"]
</code></pre>
<p>Everything in the builder stage — TypeScript, the full dependency tree, source files, build caches — is thrown away. Only <code>dist/</code> crosses the boundary.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a166d51da253d50d4118b39/d42a6392-1cd3-4e59-b98a-4f0d3956116f.svg" alt="A two-stage Docker build where the builder stage installs all dependencies and compiles TypeScript, then only the dist folder crosses into a lean runtime stage that installs production dependencies alone, with the entire builder discarded" style="display:block;margin:0 auto" />

<p>A few details in there that actually matter:</p>
<ul>
<li><strong><code>npm ci</code>, not <code>npm install</code>.</strong> It installs exactly what's in your lockfile, and it's faster. <code>npm install</code> can silently update your lockfile mid-build, which means your CI image and your teammate's image aren't the same thing.</li>
<li><strong><code>--omit=dev</code> in the runtime stage.</strong> This is where most of the savings live. On a typical TypeScript API, devDependencies are frequently larger than production dependencies.</li>
<li><strong><code>USER node</code>.</strong> Containers run as root by default. There's no reason your Node process needs to be root, and it costs you one line not to be.</li>
<li><strong><code>ENV NODE_ENV=production</code></strong> — some libraries check this at runtime and skip development-only work.</li>
</ul>
<h2>Fix three: pick a smaller base image</h2>
<p>Rough sizes for the base image alone, before your code:</p>
<table>
<thead>
<tr>
<th>Base image</th>
<th>Approx. size</th>
<th>When I'd use it</th>
</tr>
</thead>
<tbody><tr>
<td><code>node:20</code></td>
<td>~1.1GB</td>
<td>Almost never for production</td>
</tr>
<tr>
<td><code>node:20-slim</code></td>
<td>~240MB</td>
<td>Native modules that fight with musl</td>
</tr>
<tr>
<td><code>node:20-alpine</code></td>
<td>~135MB</td>
<td>My default</td>
</tr>
<tr>
<td><code>distroless/nodejs20</code></td>
<td>~150MB</td>
<td>Locked-down environments, no shell</td>
</tr>
</tbody></table>
<p>Alpine is my default, but I want to be honest about the trade-off, because "just use Alpine" advice usually skips it.</p>
<p>Alpine uses musl libc instead of glibc. Most things don't care. Some things care a lot — native modules that ship prebuilt glibc binaries have to compile from source instead, which makes builds slower and occasionally breaks outright. <code>sharp</code>, <code>canvas</code>, some database drivers, and anything wrapping a C library are the usual suspects. If you hit that wall, <code>node:20-slim</code> is the sensible retreat: still a fraction of the full image, still glibc.</p>
<p>And distroless is worth knowing about even if you don't use it. It has no shell, no package manager, nothing but the runtime. Great security posture, genuinely unpleasant to debug — you can't <code>docker exec</code> into a shell, because there isn't one.</p>
<h2>What the numbers actually look like</h2>
<p>Ballpark figures for a typical TypeScript Express or Nest API — your mileage will vary with your dependency tree, but the <em>shape</em> holds:</p>
<table>
<thead>
<tr>
<th>Version</th>
<th>Approx. size</th>
</tr>
</thead>
<tbody><tr>
<td><code>node:20</code>, no <code>.dockerignore</code>, dev deps included</td>
<td>~1.2GB</td>
</tr>
<tr>
<td><code>node:20-slim</code>, multi-stage, prod deps only</td>
<td>~380MB</td>
</tr>
<tr>
<td><code>node:20-alpine</code>, multi-stage, prod deps only</td>
<td>~180MB</td>
</tr>
</tbody></table>
<p>Which brings me to a claim I want to push back on. You'll see posts promising sub-50MB Node images. Be sceptical. The Node runtime itself is roughly 120MB on Alpine — you cannot get under that and still run Node. If someone's showing you 40MB, they've either switched to a compiled runtime like Bun or Go, or they're quoting the <em>compressed</em> registry size rather than the on-disk size. Both are fine things to do. Neither is the same claim.</p>
<p><strong>180MB is a realistic, honest target for a Node service.</strong> Chasing lower usually means fighting your runtime rather than fixing your image.</p>
<h2>The layer-order trick that speeds up every build</h2>
<p>One more thing, and it's about time rather than size, but it's the change I appreciate most day to day.</p>
<p>Docker caches each instruction as a layer. When one layer changes, every layer after it rebuilds. So this ordering is quietly expensive:</p>
<pre><code class="language-dockerfile">COPY . .
RUN npm ci        # ← re-runs on EVERY code change
</code></pre>
<p>And this one isn't:</p>
<pre><code class="language-dockerfile">COPY package*.json ./
RUN npm ci        # ← only re-runs when dependencies change
COPY . .
</code></pre>
<p>Same instructions, different order. In the second version, editing a controller doesn't reinstall your dependencies — Docker reuses the cached layer and skips straight to copying your source. On a large dependency tree, that's the difference between a two-minute CI build and a fifteen-second one.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a166d51da253d50d4118b39/74055e68-29c1-4fe4-8467-96ff8de4af8a.svg" alt="Two build orders compared: copying source before installing dependencies invalidates the cache on every code change and reinstalls everything, while copying package.json first keeps the install layer cached and only re-copies the changed source" style="display:block;margin:0 auto" />

<p>The rule of thumb: <strong>put the things that change least at the top of your Dockerfile, and the things that change most at the bottom.</strong> Your dependencies change weekly. Your source changes hourly. Order accordingly.</p>
<h2>My personal take</h2>
<p>Image size is one of those problems that feels cosmetic until it isn't.</p>
<p>A 1.2GB image is slower to push, slower to pull, slower to cold-start on every autoscaling event, more expensive to store across every tag you keep, and — the part people underrate — it has a much bigger attack surface. Every package in that Debian base is something a scanner can flag and something you're nominally responsible for patching. Shipping a compiler and a package manager into production isn't just wasteful; it's a bigger blast radius if something does get in.</p>
<p>None of the fixes above are clever. Multi-stage builds, a smaller base, a <code>.dockerignore</code>, and sensible layer ordering. It's maybe twenty minutes of work and it's the highest-leverage twenty minutes in most Dockerfiles I've read.</p>
<p>If you take one thing from this: go run <code>docker images</code> right now and look at the number. If it starts with a <code>1</code>, you already know what to do.</p>
<h2>The short version</h2>
<ul>
<li>Full <code>node:20</code> is ~1.1GB before your code. <code>node:20-alpine</code> is ~135MB. That's most of your problem.</li>
<li>Multi-stage builds let you compile with the full toolchain and ship only the output.</li>
<li><code>npm ci --omit=dev</code> in the runtime stage — devDependencies are often bigger than prod ones.</li>
<li>A <code>.dockerignore</code> keeps <code>.git</code>, <code>node_modules</code> and <code>.env</code> out, and keeps your build cache alive.</li>
<li>Copy <code>package*.json</code> and install <em>before</em> copying source, so code changes don't reinstall dependencies.</li>
<li>~180MB is an honest target for a Node service. Sub-50MB claims are usually a different runtime or a compressed size.</li>
</ul>
<hr />
<p><em>Written by the TechKis team — an AI-first engineering studio. <a href="https://techkis.tech">techkis.tech</a></em></p>
]]></content:encoded></item><item><title><![CDATA[Why Full-Stack Developers Are Becoming AI Workflow Engineers]]></title><description><![CDATA[For years, becoming a full-stack developer was considered one of the most valuable career paths in software engineering.
A single developer could build frontend applications, backend services, APIs, d]]></description><link>https://blogs.techkis.tech/why-full-stack-developers-are-becoming-ai-workflow-engineers</link><guid isPermaLink="true">https://blogs.techkis.tech/why-full-stack-developers-are-becoming-ai-workflow-engineers</guid><category><![CDATA[Artificial Intelligence]]></category><category><![CDATA[software development]]></category><category><![CDATA[AI Coding Tools]]></category><category><![CDATA[Software Engineering]]></category><category><![CDATA[developer productivity]]></category><category><![CDATA[Programming Blogs]]></category><category><![CDATA[generative ai]]></category><category><![CDATA[#career in tech]]></category><category><![CDATA[Future of Software Development]]></category><category><![CDATA[AI for Developers]]></category><dc:creator><![CDATA[TechKis]]></dc:creator><pubDate>Mon, 29 Jun 2026 17:23:53 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a2c3ef0a04c0a66c61a937e/4ada7e1b-2a9d-49a8-858d-893060b31457.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>For years, becoming a full-stack developer was considered one of the most valuable career paths in software engineering.</p>
<p>A single developer could build frontend applications, backend services, APIs, databases, authentication systems, and deployment pipelines.</p>
<p>That skillset remains incredibly valuable today.</p>
<p>However, the rapid adoption of AI tools is changing what developers spend their time doing.</p>
<p>Developers are no longer responsible only for writing code.</p>
<p>Increasingly, they are responsible for orchestrating workflows between AI models, business systems, APIs, databases, and automation platforms.</p>
<p>This shift is creating a new type of engineer.</p>
<p>The AI Workflow Engineer.</p>
<hr />
<h3>Why Full-Stack Development Is Changing</h3>
<p>Traditional full-stack development focused on building software applications.</p>
<p>A typical workflow looked something like this:</p>
<ul>
<li><p>Frontend</p>
</li>
<li><p>Backend</p>
</li>
<li><p>Database</p>
</li>
<li><p>APIs</p>
</li>
<li><p>Deployment</p>
</li>
</ul>
<p>Developers wrote most of the code manually and controlled every layer of the application stack.</p>
<p>Today, AI tools can generate UI components, API endpoints, database schemas, documentation, and even test cases.</p>
<p>As a result, the bottleneck is no longer writing code.</p>
<p>The bottleneck is connecting systems together and designing workflows that produce reliable outcomes.</p>
<p>Developers are spending less time coding every feature from scratch and more time coordinating how different systems interact.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/6a2c3ef0a04c0a66c61a937e/462f4383-653b-4ec0-89af-86d29278be6b.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h3>What Is An AI Workflow Engineer?</h3>
<p>An AI Workflow Engineer focuses on designing how AI systems interact with the rest of the technology ecosystem.</p>
<p>Instead of simply building features, they build processes.</p>
<p>A modern AI workflow may include:</p>
<ul>
<li><p>Large Language Models</p>
</li>
<li><p>Vector Databases</p>
</li>
<li><p>APIs</p>
</li>
<li><p>Business Systems</p>
</li>
<li><p>Automation Tools</p>
</li>
<li><p>Memory Systems</p>
</li>
<li><p>MCP Servers</p>
</li>
</ul>
<p>The engineer's responsibility is ensuring all of these components work together efficiently.</p>
<p>This requires a different mindset.</p>
<p>The goal is no longer writing every line of code.</p>
<p>The goal is creating intelligent systems that can perform tasks autonomously and reliably.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/6a2c3ef0a04c0a66c61a937e/bf81be70-57e6-4147-b8a9-9b4fbb351494.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h3>The New Skills Developers Need</h3>
<p>As AI becomes integrated into software development, the most valuable skills are evolving.</p>
<p>Developers still need strong programming fundamentals.</p>
<p>But they also need to understand:</p>
<ul>
<li><p>Context Engineering</p>
</li>
<li><p>AI Agents</p>
</li>
<li><p>MCP</p>
</li>
<li><p>Workflow Design</p>
</li>
<li><p>System Architecture</p>
</li>
<li><p>Automation Platforms</p>
</li>
<li><p>Prompt Design</p>
</li>
<li><p>Data Pipelines</p>
</li>
</ul>
<p>The developers who combine software engineering knowledge with AI workflow design will have a significant advantage over those who focus only on code generation.</p>
<p>Coding remains important.</p>
<p>But understanding how systems communicate is becoming even more valuable.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/6a2c3ef0a04c0a66c61a937e/dcc83f56-18e2-4191-92e5-2640af62c42b.png" alt="" style="display:block;margin:0 auto" />

<h3>What Happens Next?</h3>
<p>The future of software engineering is unlikely to be a choice between developers and AI.</p>
<p>Instead, it will be about developers who know how to leverage AI effectively.</p>
<p>Companies are already looking for engineers who can integrate AI into existing workflows, automate repetitive tasks, and build intelligent systems that connect with real-world business processes.</p>
<p>The developers who adapt to this shift will be positioned to build the next generation of software products.</p>
<p>The title on the job description may still say "Software Engineer."</p>
<p>But the responsibilities are increasingly starting to look like those of an AI Workflow Engineer.</p>
<hr />
<h3>The Short Version</h3>
<ul>
<li><p>Full-stack development remains valuable, but the role is evolving.</p>
</li>
<li><p>AI tools are reducing the amount of manual coding required.</p>
</li>
<li><p>Developers are increasingly responsible for orchestrating workflows between AI and business systems.</p>
</li>
<li><p>AI Workflow Engineering combines software engineering, automation, architecture, and AI.</p>
</li>
<li><p>Skills like MCP, Context Engineering, AI Agents, and workflow design are becoming increasingly important.</p>
</li>
<li><p>The future belongs to developers who can connect systems, not just write code.</p>
</li>
</ul>
<hr />
<p>Written by the TechKis team — an AI-first engineering studio. Building AI products, web platforms, mobile apps, and custom software. Need help bringing your idea to life? Let's build it together. <a href="http://techkis.tech">techkis.tech</a></p>
]]></content:encoded></item><item><title><![CDATA[Understanding MCP (Model Context Protocol): The USB-C of AI Applications]]></title><description><![CDATA[If you've been exploring the AI ecosystem recently, you've probably noticed one term appearing more frequently across developer communities, AI startups, and enterprise platforms: MCP.
Short for Model]]></description><link>https://blogs.techkis.tech/understanding-mcp-model-context-protocol-the-usb-c-of-ai-applications</link><guid isPermaLink="true">https://blogs.techkis.tech/understanding-mcp-model-context-protocol-the-usb-c-of-ai-applications</guid><category><![CDATA[Model Context Protocol]]></category><category><![CDATA[mcp]]></category><category><![CDATA[Artificial Intelligence]]></category><category><![CDATA[AI development]]></category><category><![CDATA[ai agents]]></category><category><![CDATA[large language models]]></category><category><![CDATA[Developer Tools]]></category><category><![CDATA[Software Engineering]]></category><category><![CDATA[ai integration]]></category><category><![CDATA[#anthropic]]></category><dc:creator><![CDATA[TechKis]]></dc:creator><pubDate>Mon, 29 Jun 2026 17:22:28 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a2c3ef0a04c0a66c61a937e/90fa647d-e8b0-4d22-9275-d3edd79db052.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you've been exploring the AI ecosystem recently, you've probably noticed one term appearing more frequently across developer communities, AI startups, and enterprise platforms: <strong>MCP</strong>.</p>
<p>Short for <strong>Model Context Protocol</strong>, MCP is rapidly becoming one of the most important standards in modern AI development.</p>
<p>At first glance, it may look like just another protocol. But many engineers believe MCP could fundamentally change how AI systems interact with software, databases, APIs, and business tools.</p>
<p>To understand its significance, imagine a world where every device required a completely different cable. Connecting a monitor, keyboard, charger, and external drive would require separate standards and custom adapters.</p>
<p>That problem was largely solved by <strong>USB-C</strong>.</p>
<p>MCP is attempting to solve a very similar problem for AI applications.</p>
<hr />
<h3>Why AI Integrations Became So Complicated</h3>
<p>One of the biggest challenges in AI development today is <strong>connectivity</strong>.</p>
<p>Large language models are incredibly powerful, but they know nothing about your company's databases, GitHub repositories, ticketing systems, documentation platforms, or internal APIs.</p>
<p>To make AI useful, developers need a way to connect these systems together. Historically, this meant building custom integrations for every service an AI application needed to access.</p>
<p>A GitHub connector.</p>
<p>A Slack connector.</p>
<p>A Jira connector.</p>
<p>A CRM connector.</p>
<p>A database connector.</p>
<p>Individually, these integrations seem manageable. But as organizations adopt more AI-powered tools, the number of integrations grows rapidly.</p>
<p>The result is increased technical complexity, duplicated engineering effort, and systems that become difficult to maintain and scale.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/6a2c3ef0a04c0a66c61a937e/2e47f880-4d38-45d5-b61e-414eea26361a.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h3>What Exactly Is MCP?</h3>
<p><strong>Model Context Protocol (MCP)</strong> introduces a standardized way for AI systems to communicate with external tools and services.</p>
<p>Instead of building custom integrations for every application, developers can expose capabilities through MCP-compatible servers. The AI system only needs to understand a single protocol: MCP.</p>
<p>Once connected, it can discover available resources, access information, execute actions, and interact with multiple systems through a common interface.</p>
<p>This dramatically reduces integration complexity while making AI applications easier to build, maintain, and scale.</p>
<p>More importantly, MCP creates interoperability. Developers can build integrations once and reuse them across multiple AI applications instead of reinventing the same connections repeatedly.</p>
<p>This is the same reason <strong>USB-C</strong> became so successful in hardware.</p>
<p>When everyone follows the same standard, everything becomes easier to connect.</p>
<p>And that's exactly what MCP aims to achieve for AI applications.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/6a2c3ef0a04c0a66c61a937e/3b3345a5-9d05-4006-bbd3-b39b5bc47ee1.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h3>Why MCP Matters For AI Agents</h3>
<p>The rise of <strong>AI agents</strong> is making protocols like MCP more important than ever.</p>
<p>Traditional chatbots are primarily designed to answer questions. AI agents, however, are built to take action.</p>
<p>They can create tickets, update documents, review code, trigger workflows, execute tasks, and coordinate multiple tools simultaneously.</p>
<p>To perform these actions effectively, agents need reliable access to external systems. Without a standardized protocol, every new integration adds complexity, increases maintenance effort, and slows down development.</p>
<p>This is where MCP comes in.</p>
<p>MCP provides a common language between AI systems and the software ecosystem around them. It allows agents to discover capabilities, access resources, and interact with multiple tools through a consistent interface.</p>
<p>Instead of spending time building hundreds of custom integrations, developers can focus on creating smarter and more capable AI applications.</p>
<p>As AI agents become more autonomous, MCP is likely to become a foundational layer of the modern AI stack.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/6a2c3ef0a04c0a66c61a937e/adf3f7e4-81f9-4df8-bd6a-c0ebc94b8dd2.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h3>What Happens Next?</h3>
<p>We're still in the early stages of MCP adoption.</p>
<p>Not every platform supports it yet, and not every organization has embraced it. However, the momentum behind MCP continues to grow as more companies look for reliable ways to connect AI systems with their existing software ecosystems.</p>
<p>The industry is rapidly moving toward <strong>AI-native applications</strong>, while AI agents are becoming more autonomous and capable of performing real-world tasks. To unlock their full potential, these systems need secure, standardized access to tools, services, databases, and business workflows.</p>
<p>This is where MCP becomes important.</p>
<p>The future of AI will not be determined only by how intelligent models become. It will also depend on how effectively those models can communicate, collaborate, and take action across the digital world.</p>
<p>Just as USB-C became a universal standard for hardware connectivity, MCP has the potential to become the universal standard for AI connectivity.</p>
<p>And that's exactly why so many developers are paying attention to it today.</p>
<hr />
<h3>The Short Version</h3>
<ul>
<li><p><strong>MCP</strong> stands for <strong>Model Context Protocol</strong>.</p>
</li>
<li><p>It provides a standardized way for AI systems to connect with external tools and services.</p>
</li>
<li><p>MCP reduces the complexity of building and maintaining custom integrations.</p>
</li>
<li><p>It enables AI applications to discover, access, and interact with external systems more efficiently.</p>
</li>
<li><p>The rise of AI agents is one of the biggest drivers behind MCP adoption.</p>
</li>
<li><p>Many developers view MCP as the <strong>USB-C standard for AI applications</strong>.</p>
</li>
</ul>
<hr />
<p>Written by the TechKis team — an AI-first engineering studio. Building AI products, web platforms, mobile apps, and custom software. Need help bringing your idea to life? Let's build it together. <a href="http://techkis.tech">techkis.tech</a></p>
]]></content:encoded></item><item><title><![CDATA[React Context API vs Redux Toolkit — Which One I Prefer and Why]]></title><description><![CDATA[State management in React can get confusing very quickly. At first everything feels simple. You keep state inside a component, pass props to children, maybe lift state up when needed. For small apps, ]]></description><link>https://blogs.techkis.tech/react-context-api-vs-redux-toolkit-which-one-i-prefer-and-why</link><guid isPermaLink="true">https://blogs.techkis.tech/react-context-api-vs-redux-toolkit-which-one-i-prefer-and-why</guid><category><![CDATA[React]]></category><category><![CDATA[React]]></category><category><![CDATA[Redux]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[Frontend Development]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[TechKis]]></dc:creator><pubDate>Fri, 12 Jun 2026 07:32:42 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a166d51da253d50d4118b39/5a5ccb66-5ef0-4a19-82d3-300dfab4039f.svg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>State management in React can get confusing very quickly. At first everything feels simple. You keep state inside a component, pass props to children, maybe lift state up when needed. For small apps, that works perfectly fine.</p>
<p>But once the app grows, the same question keeps coming back: should I use Context API or Redux Toolkit?</p>
<p>I've seen developers argue about this a lot. Some say Context is enough. Some say Redux is the proper way. Honestly, I don't think one answer fits every project. It depends on the size of the app, the type of state, and how often that state changes.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a166d51da253d50d4118b39/8f9b455b-19d7-4f1f-add3-0f83df77a2e2.svg" alt="Context API built into React for simple shared state, next to Redux Toolkit with slices and DevTools for complex app state" style="display:block;margin:0 auto" />

<h2>Context API is great when the state is simple</h2>
<p>React Context is built into React — that's already a big advantage. No extra package, no additional setup, no separate store configuration. For simple shared data, Context works nicely: logged-in user info, the selected theme, language preference, basic app settings, small global values.</p>
<p>In these cases Context feels clean and lightweight. You create a provider, wrap your app, and use the value wherever you need it. Simple. And for many apps, that's enough.</p>
<h2>But Context can become messy when the app grows</h2>
<p>The problem starts when Context is used for <em>everything</em> — authentication state, cart state, filters, API data, form state, notification state, dashboard data. Suddenly you have multiple providers. State updates become harder to track. Components re-render more than expected. Debugging becomes annoying. And the app slowly becomes difficult to reason about.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a166d51da253d50d4118b39/884d9391-117d-4533-86f2-c724ad04a676.svg" alt="Wrapping the app in seven nested Context providers, where unrelated components re-render and state updates get hard to trace" style="display:block;margin:0 auto" />

<p>That's where Context starts showing its limits. Not because Context is bad, but because it was never meant to become a full state management system for complex apps.</p>
<h2>Redux Toolkit feels better for serious app state</h2>
<p>Redux had a reputation for being heavy earlier — too much boilerplate, too many files, too many concepts. But Redux Toolkit changed that a lot. Now it feels much cleaner. You create slices, write reducers more easily, and manage async logic with <code>createAsyncThunk</code> or RTK Query. And most importantly, you get a predictable state flow.</p>
<p>That matters when your app gets bigger — ecommerce carts, dashboards, complex filters, user permissions, API-driven state, multi-step forms, large admin panels. In these cases Redux Toolkit feels more organized.</p>
<h2>The biggest advantage of Redux Toolkit is structure</h2>
<p>This is why I personally prefer Redux Toolkit for bigger applications. It gives structure. You know where state lives. You know how it changes. You can trace actions, debug with Redux DevTools, and separate logic into slices.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a166d51da253d50d4118b39/277958ee-5046-4429-8772-f7705ee070c3.svg" alt="Auth, cart, filters, and permissions slices combine into one store, with a one-way dispatch-to-selector flow and Redux DevTools recording every action for time-travel debugging" style="display:block;margin:0 auto" />

<p>That makes the application easier to maintain, especially when multiple developers are working on the same project. Context can work, but Redux Toolkit gives discipline. And in real projects, discipline matters.</p>
<h2>So which one do I prefer?</h2>
<p>For small apps, I prefer Context API. It's simple, fast, no extra dependency, and no need to over-engineer.</p>
<p>But for medium or large applications, I prefer Redux Toolkit — especially when the app has multiple screens, API data, user roles, cart logic, filters, or shared state used in many places. Because once state becomes important to the product, I want something predictable, something easier to debug, something that doesn't become messy after six months. For that, Redux Toolkit feels safer.</p>
<h2>My personal rule</h2>
<p>I usually think about it like this:</p>
<ul>
<li>If the state changes rarely and is simple, <strong>Context is fine</strong>.</li>
<li>If the state changes often, affects many components, or needs debugging, <strong>Redux Toolkit is better</strong>.</li>
</ul>
<p>That simple rule avoids a lot of confusion. Because the real mistake isn't choosing Context or Redux — it's using the wrong tool for the wrong kind of state.</p>
<h2>The short version</h2>
<ul>
<li>Context API is best for simple shared values like theme, auth user, or language.</li>
<li>Redux Toolkit is better for larger apps with complex and frequently changing state.</li>
<li>Context is lightweight but can become messy when overused.</li>
<li>Redux Toolkit gives better structure, debugging, and predictable state flow.</li>
<li>My preference: Context for small apps, Redux Toolkit for serious production apps.</li>
</ul>
<hr />
<p><em>Written by the TechKis team — an AI-first engineering studio. <a href="https://techkis.tech">techkis.tech</a></em></p>
]]></content:encoded></item><item><title><![CDATA[RabbitMQ vs Database Queues — Which One Should You Use?]]></title><description><![CDATA[If you've built backend systems long enough, sooner or later you run into queues. Sending emails, processing notifications, generating reports, syncing data with third-party APIs, handling background ]]></description><link>https://blogs.techkis.tech/rabbitmq-vs-database-queues-which-one-should-you-use</link><guid isPermaLink="true">https://blogs.techkis.tech/rabbitmq-vs-database-queues-which-one-should-you-use</guid><category><![CDATA[backend]]></category><category><![CDATA[rabbitmq]]></category><category><![CDATA[Databases]]></category><category><![CDATA[System Design]]></category><category><![CDATA[scalability]]></category><dc:creator><![CDATA[TechKis]]></dc:creator><pubDate>Fri, 12 Jun 2026 07:25:06 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a166d51da253d50d4118b39/66488651-b417-4c69-bf2d-6d5ca6b7f671.svg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you've built backend systems long enough, sooner or later you run into queues. Sending emails, processing notifications, generating reports, syncing data with third-party APIs, handling background jobs that shouldn't block the user request.</p>
<p>That's usually the moment the real question shows up: should I just use a database queue, or is it time to move to RabbitMQ?</p>
<p>I've thought about this a lot while working on backend-heavy applications. And honestly, the answer usually depends less on what's "better" technically and more on what your application actually needs. Both work. They just solve slightly different problems.</p>
<h2>Database queues are often the easiest place to start</h2>
<p>For many applications, database queues are enough — especially in the beginning. If you're using something like Laravel queues backed by a <code>jobs</code> table, setup is straightforward. No extra infrastructure, no separate message broker, no queue server to manage. Everything stays inside the application environment, which makes development fast. And that simplicity matters.</p>
<p>For things like sending emails, OTP delivery, notifications, invoice generation, and small background jobs, database queues usually do the job perfectly fine. We sometimes overcomplicate things too early.</p>
<h2>But as traffic grows, the database starts doing too much</h2>
<p>This is where things begin to feel heavier. Your database is already handling user requests, inserts, updates, joins, reporting queries, and search filters. Then queue jobs get added on top, and suddenly the same DB is also responsible for processing background tasks.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a166d51da253d50d4118b39/ab0a1c8d-37cc-4a04-b1b6-96c71f6b4465.svg" alt="A single database already serving requests, inserts, joins, and reports hits 92% load when queue jobs pile on, and jobs start backing up with retries" style="display:block;margin:0 auto" />

<p>That works for a while — until the queue volume increases. Then you start noticing jobs piling up, slower processing, delayed workers, increased DB load, and retries creating even more pressure. At that point the queue system itself becomes part of the performance conversation. And that's usually where RabbitMQ enters the picture.</p>
<h2>RabbitMQ feels built for messaging from day one</h2>
<p>RabbitMQ approaches the problem differently. Instead of storing jobs inside the database, it acts as a dedicated message broker. Its only job is moving messages between producers and consumers efficiently.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a166d51da253d50d4118b39/fc616c91-2974-4e00-af1b-83b2ced11a49.svg" alt="A producer publishes to a RabbitMQ exchange that routes messages by key into separate email, notification, and order queues, each consumed by independent workers, with a dead-letter queue for failures while the database stays focused on data" style="display:block;margin:0 auto" />

<p>That separation helps a lot. Your app pushes a message, RabbitMQ handles routing, and workers consume it independently. The database stays focused on data; queue processing stays focused on messages.</p>
<p>That separation becomes valuable when your workload gets heavier — high-volume notifications, real-time event processing, order pipelines, multiple worker services, microservice communication, and external integrations happening continuously. This is where RabbitMQ starts feeling stronger.</p>
<h2>The trade-off: RabbitMQ gives power, but adds complexity</h2>
<p>This part matters. RabbitMQ is powerful, but it isn't free. You're adding more infrastructure, which means server setup, deployment config, monitoring, worker management, connection handling, retry strategy, dead-letter queues, and failure recovery.</p>
<p>Compared to database queues, that's definitely more to manage. So if your application only sends the occasional background email, RabbitMQ may actually be unnecessary overhead. And that's okay — not every project needs distributed messaging infrastructure.</p>
<h2>So which one would I choose?</h2>
<p>If I'm building a <strong>small to medium application</strong>, I'd usually start with database queues. Fast setup, easy maintenance, works well, and less infrastructure to think about.</p>
<p>If I'm building a <strong>high-volume system with heavy background processing</strong>, I'd lean toward RabbitMQ — especially when thousands of jobs are processed regularly, multiple services communicate with each other, queue throughput matters, the database load is already high, and reliable async messaging is becoming core to the platform.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a166d51da253d50d4118b39/a3349ef5-bbce-4c3b-84c0-505aeb2d6b4c.svg" alt="Start with database queues for small-to-medium apps with occasional jobs; move to RabbitMQ when volume, service-to-service traffic, throughput, and DB load grow — decided by one question" style="display:block;margin:0 auto" />

<h2>My personal take</h2>
<p>I don't see RabbitMQ and database queues as competitors. I see them as stages. Database queues are a great starting point. RabbitMQ becomes valuable when scale and complexity demand it.</p>
<p>A lot of teams jump to advanced infrastructure too early, and sometimes the simpler solution is the better engineering choice. At the same time, once your queue load becomes part of your bottleneck, a dedicated message broker can make a huge difference.</p>
<p>So for me the decision usually comes down to one question: is the queue just supporting the app, or is async processing becoming part of the system itself? That answer usually makes the choice clear.</p>
<h2>The short version</h2>
<ul>
<li>Database queues are simple, fast to implement, and great for many applications.</li>
<li>RabbitMQ is built specifically for messaging and handles high-volume async processing better.</li>
<li>Database queues work well for emails, notifications, and smaller background tasks.</li>
<li>RabbitMQ becomes useful when scale, throughput, and service-to-service communication grow.</li>
<li>If your current queue isn't causing problems yet, database queues may still be the right choice.</li>
</ul>
<hr />
<p><em>Written by the TechKis team — an AI-first engineering studio. <a href="https://techkis.tech">techkis.tech</a></em></p>
]]></content:encoded></item><item><title><![CDATA[I Tried Google Antigravity IDE — And It Feels Different From Traditional Coding Tools]]></title><description><![CDATA[Over the past few months, AI coding tools have been everywhere. Cursor, GitHub Copilot, Windsurf, Codeium — every few weeks there's something new claiming it will completely change how developers writ]]></description><link>https://blogs.techkis.tech/i-tried-google-antigravity-ide-and-it-feels-different-from-traditional-coding-tools</link><guid isPermaLink="true">https://blogs.techkis.tech/i-tried-google-antigravity-ide-and-it-feels-different-from-traditional-coding-tools</guid><category><![CDATA[Artificial Intelligence]]></category><category><![CDATA[Google]]></category><category><![CDATA[Developer Tools]]></category><category><![CDATA[IDEs]]></category><category><![CDATA[Programming Blogs]]></category><category><![CDATA[Programming Tips]]></category><category><![CDATA[AI]]></category><category><![CDATA[#ai-tools]]></category><category><![CDATA[ai agents]]></category><dc:creator><![CDATA[TechKis]]></dc:creator><pubDate>Fri, 12 Jun 2026 07:14:50 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a166d51da253d50d4118b39/570e346f-aee1-4738-a6a4-57c530948554.svg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Over the past few months, AI coding tools have been everywhere. Cursor, GitHub Copilot, Windsurf, Codeium — every few weeks there's something new claiming it will completely change how developers write code.</p>
<p>So when I came across Google Antigravity, I was curious. At first I assumed it was just another editor with AI autocomplete built in. But the more I explored it, the more it felt like Google is thinking about development in a slightly different way. And that's what caught my attention.</p>
<p><em>(Quick context: Antigravity launched in public preview, free for individuals, and runs on macOS, Windows and Linux. It's built around Gemini 3 Pro but also supports Anthropic's Claude Sonnet 4.5 and OpenAI's GPT-OSS.)</em></p>
<h2>It doesn't feel like just another IDE</h2>
<p>Most IDEs still work the same way. You open files, write code, run terminal commands, debug something, fix it, repeat. AI usually sits <em>beside</em> that workflow as an assistant — maybe it completes code, maybe it writes a function, maybe it explains an error.</p>
<p>Antigravity feels like it's trying to move AI deeper into the workflow itself. Not just helping with code, but helping with the entire task. That difference sounds small, but the more you read into it, the bigger it gets.</p>
<h2>The most interesting part: agent-first development</h2>
<p>This is the part that stood out most to me. Google describes Antigravity as <em>agent-first</em> development, and honestly that explains it pretty well. Instead of AI helping with a line of code, the idea feels closer to AI helping with the job you're trying to complete.</p>
<p>For example: creating a new feature, updating multiple files together, debugging a failing implementation, running checks, and reviewing the generated output. There's even a dedicated "Manager" surface where you can spawn and watch several agents work in parallel, each with direct access to the editor, terminal and browser.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a166d51da253d50d4118b39/929f88e4-88dd-4b06-83b0-7ac8d93b558f.svg" alt="From the prompt 'add a dark mode toggle', the Antigravity agent produces a task plan and works through it across four files, running tests and taking browser screenshots to validate its own work" style="display:block;margin:0 auto" />

<p>It feels less like autocomplete and more like having another developer inside the project with context. That's a pretty interesting direction.</p>
<h2>It still feels familiar, which I like</h2>
<p>One thing I noticed immediately: Google doesn't seem to be forcing developers into a completely new way of working. And that matters, because most developers already have workflows they're comfortable with — editor, terminal, Git, browser, docs, local environment. That workflow already works.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a166d51da253d50d4118b39/462e9d92-59f5-4cee-8594-67d882b47e86.svg" alt="The everyday tools — editor, terminal, Git, browser, docs — stay where they are, with the Antigravity agent layer connecting to all of them on top instead of replacing them" style="display:block;margin:0 auto" />

<p>Antigravity feels more like an extra layer on top of that rather than asking you to replace everything. Personally, I think that makes adoption much easier. Good developer tools usually fit into your workflow. They don't fight against it.</p>
<h2>The real test will be production projects</h2>
<p>This is where I think every AI coding tool eventually gets tested. Demos always look clean. Real codebases usually aren't.</p>
<p>Production projects are messy. There's old code, temporary fixes, unclear naming, large folders nobody wants to touch, and half-finished documentation. That's where tools like this either become genuinely useful — or become something you open once and never come back to.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a166d51da253d50d4118b39/2c87bfbc-95f2-47a3-9d87-ed128417918c.svg" alt="A clean demo repo next to a messy production tree full of legacy folders, hacks, and TODOs, with the agent asking which file holds the real auth logic" style="display:block;margin:0 auto" />

<p>What I'm curious about is how Antigravity handles those real-world environments, because that's where developers actually spend their time.</p>
<h2>My take after exploring it</h2>
<p>I don't think Antigravity is trying to replace developers. And honestly, I don't think that's what most developers want anyway.</p>
<p>What feels more realistic is this: AI handles more of the repetitive movement around coding, and developers spend more time on decisions — architecture, reviewing, thinking through product and implementation. Less context switching, less jumping between tabs, less repetitive setup work. More actual building. That's the part I find interesting.</p>
<h2>The short version</h2>
<ul>
<li>Google Antigravity feels bigger than a normal AI code editor.</li>
<li>It's focused on agent-first development, not only autocomplete.</li>
<li>The goal seems to be helping developers complete tasks, not just write code.</li>
<li>It still feels close to existing workflows, which makes it easier to approach.</li>
<li>The real test will be how useful it becomes inside real production codebases.</li>
</ul>
<hr />
<p><em>Written by the TechKis team — an AI-first engineering studio. <a href="https://techkis.tech">techkis.tech</a></em></p>
]]></content:encoded></item><item><title><![CDATA[Will AI Replace Developers? My Honest Take as a Software Engineer]]></title><description><![CDATA[This question comes up everywhere now. On X, on YouTube, in tech podcasts, even in random conversations between developers: "Will AI replace software engineers?" Over the past year I've heard it more ]]></description><link>https://blogs.techkis.tech/will-ai-replace-developers-my-honest-take-as-a-software-engineer</link><guid isPermaLink="true">https://blogs.techkis.tech/will-ai-replace-developers-my-honest-take-as-a-software-engineer</guid><category><![CDATA["artificial-intelligence", "programming", "software-development", "developer-tools", "career"]]></category><category><![CDATA[AI]]></category><category><![CDATA[Artificial Intelligence]]></category><category><![CDATA[Programming Blogs]]></category><category><![CDATA[Developer Tools]]></category><dc:creator><![CDATA[TechKis]]></dc:creator><pubDate>Fri, 12 Jun 2026 05:28:19 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a166d51da253d50d4118b39/d4019607-7437-4bee-b34d-0d5e2e7e377c.svg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This question comes up everywhere now. On X, on YouTube, in tech podcasts, even in random conversations between developers: <em>"Will AI replace software engineers?"</em> Over the past year I've heard it more often than almost any other tech question.</p>
<p>And honestly, I understand why. AI tools have improved fast, much faster than most people expected. You type a prompt and it writes code. You paste an error and it explains the issue. You ask for a component and it builds one, sometimes surprisingly well. Enough to make people stop and think: if AI can already do this, what happens next?</p>
<h2>My short answer? No, but things are definitely changing</h2>
<p>I don't think AI is replacing developers anytime soon. But I do think it's changing how development happens, and that's an important difference.</p>
<p>Because writing code is only one part of software development. Real development includes a lot more than syntax: understanding product requirements, deciding architecture, debugging strange production issues, handling trade-offs, thinking about performance, working with teams, and understanding what users actually need.</p>
<p>Most of that isn't "write code." It's judgment, context, experience, and sometimes even instinct. That part is much harder to automate.</p>
<h2>AI is very good at generating code</h2>
<p>That part is obvious now. Need a React component? AI can generate one. Need help with a SQL query? Usually fast. Need a boilerplate API structure? Done in seconds. Need test cases? Also possible.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a166d51da253d50d4118b39/69e96641-951c-45b3-8b25-6603f799bfdb.svg" alt="A chat prompt asks for a Button component with a loading state, and the AI returns a working React component in two seconds" style="display:block;margin:0 auto" />

<p>For repetitive work, AI is genuinely useful — sometimes very useful. And if I'm honest, I use it too. A lot of developers do now. Not because we can't code without it, but because it speeds things up.</p>
<h2>But generating code isn't the same as building software</h2>
<p>This is where the conversation usually gets oversimplified, because code generation <em>looks</em> impressive. But software engineering isn't just output.</p>
<p>Real projects are messy. Codebases are messy. Requirements change halfway through development. Clients change priorities. Legacy code exists. Production bugs happen only on one server at 2 AM. Naming conventions make no sense. Documentation is incomplete. And business logic lives partly in code... and partly inside someone's head.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a166d51da253d50d4118b39/0b67ad09-c60f-4607-84e6-c17162c63224.svg" alt="A 2 AM production incident that only happens on one server, surrounded by the messy realities AI struggles with: changing requirements, legacy code, incomplete docs, and tribal knowledge" style="display:block;margin:0 auto" />

<p>That's real software development. And AI still struggles there, because context matters more than code.</p>
<h2>The role of developers is shifting</h2>
<p>If anything, AI is changing <em>where</em> developers spend their time. Less time writing repetitive code. More time reviewing. More time designing systems. More time asking better questions. More time making decisions.</p>
<p>AI can generate options. Developers still decide which option is correct — and often which one is safest, fastest, cleanest, or easiest to maintain six months later.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a166d51da253d50d4118b39/1484c5bf-8090-4d4f-b3c5-6b9883dcbbd6.svg" alt="For a state management choice, the AI offers Context, Redux, and Zustand, and the developer picks Zustand with a reason: smallest footprint, easiest to maintain in six months" style="display:block;margin:0 auto" />

<p>That decision-making layer is still deeply human.</p>
<h2>What I think actually happens next</h2>
<p>My guess? AI becomes part of every developer workflow — kind of like Git, or Stack Overflow, or package managers. At first optional, then normal, then expected.</p>
<p>Developers who learn to use AI well will probably move faster, prototype faster, debug faster, and ship faster. Not because AI replaces them, but because it removes some of the repetitive friction around development. And the human part — thinking, deciding, building — becomes even more important.</p>
<h2>The short version</h2>
<ul>
<li>No, I don't think AI will replace developers anytime soon.</li>
<li>AI is very good at generating code, especially repetitive code.</li>
<li>But software engineering is much more than writing syntax.</li>
<li>Context, architecture, debugging, business logic, and decision-making still need developers.</li>
<li>AI will likely become a daily development tool, not a replacement for engineers.</li>
</ul>
<hr />
<p><em>Written by the TechKis team — an AI-first engineering studio. <a href="https://techkis.tech">techkis.tech</a></em></p>
]]></content:encoded></item><item><title><![CDATA[How AI Coding Tools Are Changing Software Development in 2026]]></title><description><![CDATA[A year ago, most developers still treated AI coding tools like a fun experiment. Something useful for writing boilerplate, generating a quick snippet, or fixing a regex you didn't feel like writing yo]]></description><link>https://blogs.techkis.tech/how-ai-coding-tools-are-changing-software-development-in-2026</link><guid isPermaLink="true">https://blogs.techkis.tech/how-ai-coding-tools-are-changing-software-development-in-2026</guid><category><![CDATA[#AI  #Programming  #DeveloperTools  #GitHubCopilot  #Tabnine  #Productivity  #Coding  #ArtificialIntelligence  #CodeAssistants  #SoftwareDevelopment]]></category><category><![CDATA[AI-Programming Coding-Assistance Programming-Skills Smart-Solution AI-Development Code-Optimization Machine-Learning Artificial-Intelligence Software-Development Code-Enhancement Code-Automation Developer-Tools Programming-Tutorials Code-Productivity Tech-Innovation Coding-Help AI-Tools Learn-to-Code Code-Refactoring Code-Quality]]></category><dc:creator><![CDATA[TechKis]]></dc:creator><pubDate>Fri, 12 Jun 2026 05:14:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a166d51da253d50d4118b39/75352334-fb71-4c8c-a8b6-e6ec84d156c6.svg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>A year ago, most developers still treated AI coding tools like a fun experiment. Something useful for writing boilerplate, generating a quick snippet, or fixing a regex you didn't feel like writing yourself.</p>
<p>That changed fast.</p>
<p>In 2026, AI tools aren't sitting on the side anymore. They're part of the daily workflow. And honestly, even if someone tells you they don't use AI while coding, there's a good chance they already have without thinking much about it. Autocomplete, debugging help, test generation, documentation, code review... AI has quietly become part of software development itself.</p>
<h2>It started with autocomplete, but it's no longer just that</h2>
<p>For a lot of developers, the entry point was simple. GitHub Copilot finishing a function. ChatGPT helping debug an error. Cursor rewriting some repetitive code. At first it felt like productivity support: useful, but optional.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a166d51da253d50d4118b39/377ea4c0-634b-401a-bf4b-7fd9f69003aa.svg" alt="A developer types the isValidEmail function signature and the AI completes the body as a grey ghost suggestion to accept with Tab" style="display:block;margin:0 auto" />

<p>Now it feels bigger. AI tools have moved well beyond "suggest the next line." They help developers generate components, write API routes, explain unfamiliar code, refactor old files, write unit tests, generate SQL queries, review pull requests, and summarize documentation.</p>
<p>And increasingly, they help complete entire tasks, not just snippets. That's a very different shift.</p>
<h2>Developers spend less time typing and more time thinking</h2>
<p>This is probably the biggest change I've noticed. Coding used to involve a lot of repetition: writing similar API controllers, repeating validation rules, creating DTOs, building CRUD pages, setting up routes, writing test scaffolding. Useful work, but repetitive.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a166d51da253d50d4118b39/40b6801a-1bf8-4bb0-82fe-345b8c09924a.svg" alt="One chat prompt asks for a paginated users endpoint, the AI returns three real files, and the developer is left thinking about caching, paging strategy, and rate limiting" style="display:block;margin:0 auto" />

<p>AI handles a surprising amount of that now. So developers spend more time on the things that actually need a human: architecture decisions, reviewing logic, debugging edge cases, thinking through product requirements, and deciding what should be built in the first place.</p>
<p>Less typing. More decision-making. And honestly, that feels closer to real engineering.</p>
<h2>The tools are improving faster than most people expected</h2>
<p>A year ago, AI could write code... sometimes, and often with mistakes. Now tools like Cursor, GitHub Copilot, Claude Code, Windsurf, and Google Antigravity feel far more capable.</p>
<p>Not perfect. They still get things wrong and still need review. But they're noticeably more useful than before, especially when they understand your project's context. And that context changes everything, because generating code is easy. Generating code that actually fits <em>your</em> project is the hard part. That's exactly where the newer tools are improving quickly.</p>
<h2>But AI hasn't replaced developers, and probably won't anytime soon</h2>
<p>This question comes up constantly: "Will AI replace software engineers?" Personally, I don't think that's the right question.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a166d51da253d50d4118b39/36d8af4c-c07f-4458-bccf-ca4e28d37d4a.svg" alt="A pull request shows AI-generated delete-user code, and a human review comment flags the missing authorization and ownership checks before merge" style="display:block;margin:0 auto" />

<p>AI is very good at <em>generating</em>. Developers are still needed for <em>deciding</em>. And software is full of decisions: trade-offs, business logic, edge cases, system design, security, performance, understanding users. AI can help with the code, but it still doesn't understand product thinking the way an experienced developer does. At least not yet.</p>
<p>Right now it feels more like this: AI speeds up development, developers still guide it. And that distinction matters.</p>
<h2>My personal take</h2>
<p>I think AI coding tools are becoming like Git, or Stack Overflow, or package managers. At one point each of those felt optional. Then they became normal, the kind of thing you just expect every developer to use.</p>
<p>AI coding tools are heading the same way. Not because developers are being replaced, but because the workflow is changing. The developers who adapt will probably move faster, ship faster, debug faster, and learn faster, while spending less time stuck on repetitive work.</p>
<p>That doesn't make software development <em>easier</em>. It just removes some of the friction around building software. And honestly, that's a pretty exciting shift to watch.</p>
<h2>The short version</h2>
<ul>
<li>AI coding tools are now part of the daily developer workflow.</li>
<li>They've moved beyond autocomplete into debugging, testing, refactoring, and full task completion.</li>
<li>Developers spend less time writing repetitive code and more time thinking through architecture and logic.</li>
<li>Tools like Copilot, Cursor, Claude Code, and Google Antigravity are evolving very quickly.</li>
<li>AI isn't replacing developers, but it's definitely changing how software gets built.</li>
</ul>
<hr />
<p><em>Written by the TechKis team — an AI-first engineering studio. <a href="https://techkis.tech">techkis.tech</a></em></p>
]]></content:encoded></item></channel></rss>