<?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[Surya's Blog]]></title><description><![CDATA[I'm documenting how to reason about systems — starting from fundamentals, constraints, and trade-offs.]]></description><link>https://blog.suryasathi.com</link><generator>RSS for Node</generator><lastBuildDate>Thu, 14 May 2026 22:11:27 GMT</lastBuildDate><atom:link href="https://blog.suryasathi.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Consistency Models]]></title><description><![CDATA[Background
One of the most confusing experiences when working with real systems is this:you do something, the system tells you it worked, and then immediately behaves as if it didn’t.
You update a profile photo and still see the old one.You change a ...]]></description><link>https://blog.suryasathi.com/consistency-models</link><guid isPermaLink="true">https://blog.suryasathi.com/consistency-models</guid><category><![CDATA[consistency]]></category><category><![CDATA[strong-consistency]]></category><category><![CDATA[eventual consistency]]></category><dc:creator><![CDATA[Surya Sathi]]></dc:creator><pubDate>Sun, 08 Feb 2026 18:30:31 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1769713661994/0dc02d57-92a3-45e2-bb0f-d89aca6e6d45.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-background">Background</h2>
<p>One of the most confusing experiences when working with real systems is this:<br />you do something, the system tells you it worked, and then immediately behaves as if it didn’t.</p>
<p>You update a profile photo and still see the old one.<br />You change a setting and refresh the page — nothing.</p>
<p>You refresh the screen two or three times. Nothing changes. You check again after half an hour, and suddenly it’s there.</p>
<p>The system is doing exactly what it was designed to do. We may call it a bug but the problem is that most of us don’t have a clear mental model of what the system is <em>actually promising</em>.</p>
<p>That’s where consistency comes in.</p>
<p>One might think of consistency as a binary property — either a system is consistent or it’s not. But once you start looking into distributed systems, that mental model starts breaking down quickly. Consistency isn’t a yes-or-no feature — it’s a set of tradeoffs about agreement, time, and visibility.</p>
<p>This article is my attempt to understand those tradeoffs without hiding behind buzzwords.</p>
<h2 id="heading-what-does-consistency-even-mean">What does “Consistency” even mean?</h2>
<p>At a very basic level, consistency is about agreement.</p>
<p>If multiple parts of a system are observing the same data, consistency describes if those observations agree, and how fast they agree. That’s it. It’s not about whether the data is “correct” in some moral sense. It’s about whether different observers see the same thing at the same time.</p>
<p>This distinction matters more than it sounds.</p>
<p>A system can accept a write successfully — meaning the system has <em>decided</em> that the change is valid — and still not show that change everywhere immediately. That gap between acceptance and visibility is where most confusion comes from.</p>
<p>Whenever something is written to state, there are two questions one can ask:</p>
<ol>
<li><p>Did the system accept the write?</p>
</li>
<li><p>How fast can the other component see that write?</p>
</li>
</ol>
<p>Consistency is often more about the second question.</p>
<h2 id="heading-why-consistency-feels-simple-on-one-machine">Why consistency feels simple on one machine</h2>
<p>If you’re used to single-process programs, your intuition is solid. You write to a variable, you read it back, and you get the new value. There’s no distance, no delay, no disagreement.</p>
<p>Distributed systems break this intuition because <strong>time becomes a problem</strong>.</p>
<p>Messages take time. Machines fail independently. A write that succeeds on one machine has to <em>travel</em> before other machines know about it. While that information is in transit, different parts of the system are living in different versions of reality.</p>
<p>This isn’t a rare edge case — it’s the default state of distributed systems.</p>
<h2 id="heading-replication-visibility-not-truth">Replication: visibility, not truth</h2>
<p>The databases we use are also systems themselves which have lots of nodes within them. So naturally, for backups or for better availability, we would like replicas.</p>
<p>But replication of data from one node to another takes time.</p>
<p>While replicas are catching up, they can return older data. So, you’ve now encountered inconsistency until the data is copied here — eventual consistency.</p>
<p>A subtle but important thing to know is this: replication “mostly” affects <strong>what you can see</strong>, not <strong>what is true</strong>.</p>
<p>A write is accepted by the system somewhere. Replication determines how quickly that decision becomes visible elsewhere. Confusing those two leads to a lot of incorrect reasoning about correctness.</p>
<h2 id="heading-consistency-models">Consistency Models</h2>
<h3 id="heading-strong-consistency-comforting-but-expensive-promise">Strong consistency: Comforting but expensive promise</h3>
<pre><code class="lang-mermaid">sequenceDiagram
    participant Client
    participant Leader
    participant Replica1
    participant Replica2

    Client-&gt;&gt;Leader: Write X
    Leader-&gt;&gt;Leader: Decide X is accepted
    Leader--&gt;&gt;Client: Success (write confirmed)

    Leader-&gt;&gt;Replica1: Replicate X
    Leader-&gt;&gt;Replica2: Replicate X

    Client-&gt;&gt;Replica1: Read X
    Replica1--&gt;&gt;Client: X

    Client-&gt;&gt;Replica2: Read X
    Replica2--&gt;&gt;Client: X
</code></pre>
<p>Strong consistency is the model most people assume by default, even if they don’t use the term.</p>
<p>Behaviorally, strong consistency (often meaning linearizability) means this:</p>
<p>Once a write succeeds, every subsequent read sees that write — meaning the system waits until global agreement before confirming success.</p>
<p>The cost of this promise is coordination overhead and latency.</p>
<p>To ensure that everyone agrees immediately, systems have to wait and synchronize. Latency goes up. Availability can go down. But you accept those if what matters is correctness for your use case.</p>
<p>This is the experience we expect from things which can cause panics — like bank balances, inventory counts, or account permissions. If money moved or access changed, seeing stale data is unacceptable.</p>
<h3 id="heading-eventual-consistency-acceptable-discomfort">Eventual consistency: Acceptable discomfort</h3>
<pre><code class="lang-mermaid">sequenceDiagram
    participant C as Client
    participant R1 as Replica 1
    participant R2 as Replica 2
    participant L as Leader

    Note over R1,L: Initial state: x = v1

    C-&gt;&gt;L: Write x = v2
    L--&gt;&gt;C: OK

    L--&gt;&gt;R2: Replicate x = v2
    Note over R1: Replication delayed

    C-&gt;&gt;R1: Read x
    R1--&gt;&gt;C: x = v1

    Note over L,R1: Eventually replication completes
    L--&gt;&gt;R1: Replicate x = v2
</code></pre>
<p>Under eventual consistency, the system guarantees that all observers will <em>eventually</em> converge to the same state — but not necessarily immediately.</p>
<p>The write can be accepted immediately — but its visibility can be delayed.</p>
<p>This temporary disagreement is allowed in this model.</p>
<p>Eventual consistency sounds scary until you realize how often you already rely on it.</p>
<p>Social feeds, likes, view counts, recommendations, and profile updates often use this model.</p>
<p>Seeing an outdated like count for a few seconds doesn’t break trust. Waiting long for the system to respond would result in bad user experience.</p>
<p>What’s important here is intent. Eventual consistency isn’t a failure to be strongly consistent; it’s a decision to optimize for responsiveness and availability over immediate agreement.</p>
<h3 id="heading-other-models">Other Models</h3>
<p>There are still many other models such as causal consistency, read-your-writes, monotonic reads, monotonic writes etc. These don’t replace strong or eventual consistency — they refine what a client experiences on top of them. As an example, I’ve gone through just one of them — monotonic reads below.</p>
<h3 id="heading-monotonic-reads-consistency">Monotonic Reads Consistency</h3>
<pre><code class="lang-mermaid">sequenceDiagram
    participant C as Client
    participant R1 as Replica 1
    participant R2 as Replica 2
    participant L as Leader

    Note over R1,L: Initial state: x = v1

    C-&gt;&gt;R1: Read x
    R1--&gt;&gt;C: x = v1
    Note over C: lastSeenVersion = v1

    C-&gt;&gt;L: Write x = v2
    L--&gt;&gt;C: OK
    L--&gt;&gt;R2: Replicate x = v2
    Note over R1: Replica 1 still at x = v1

    C-&gt;&gt;R2: Read x
    R2--&gt;&gt;C: x = v2
    Note over C: lastSeenVersion = v2

    C-&gt;&gt;R1: Read x (minVersion = v2)
    Note over R1: Replica 1 still at x = v1
    R1-&gt;&gt;L: Fetch x &gt;= v2
    L--&gt;&gt;R1: x = v2
    R1--&gt;&gt;C: x = v2
</code></pre>
<p>There’s another kind of inconsistency that feels especially jarring, even in systems that are otherwise acceptable.</p>
<p>You read some data.<br />Later, you read the <em>same data again</em> — and it looks older.</p>
<p>This can happen when one read request went to one replica but the next one went to another replica which was not yet updated.</p>
<p>Nothing about this violates eventual consistency in theory. The system may still converge correctly in the long run. But experientially, this feels wrong. It feels like time moved backwards.</p>
<p>This is where monotonic reads consistency comes in.</p>
<p>Monotonic reads guarantee a simple property: once you’ve observed a particular version of data, you will never see an older version of that same data again. You may not always see the latest update immediately, but you won’t regress.</p>
<p>What’s interesting is that monotonic reads are not about freshness — they’re about directionality. Humans expect systems to move forward, not backward.</p>
<p>This matters more than it initially sounds.</p>
<p>Imagine reading a comment thread, refreshing the page, and suddenly seeing fewer comments than before. Or checking order status and seeing it move from “shipped” back to “processing.” Even if the system eventually fixes itself, user trust takes a hit immediately.</p>
<h2 id="heading-consistency-is-a-product-decision-not-just-a-technical-one">Consistency is a product decision, not just a technical one</h2>
<p>One of the most useful things to remember is this: inconsistency is not a bug unless it violates expectations.</p>
<p>Showing the wrong bank balance erodes trust immediately. So it is a bug.</p>
<p>Showing an old profile picture doesn’t (unless your product requirements say otherwise). So it isn’t.</p>
<p>Systems are designed around these expectations, whether explicitly or not.</p>
<p>Once you see consistency as part of user experience and business logic — not just system internals — design decisions start making more sense.</p>
<h2 id="heading-where-this-leaves-us">Where this leaves us</h2>
<p>We now understand <em>what</em> systems promise, but not <em>how</em> they enforce those promises.</p>
<p>One more thing you can notice — databases “feel” more consistent than caches by default. The answer to why we feel that way is pretty clear once you try to rationalize the concepts we explored so far.</p>
<p>But this feeling can be misleading. Not all databases are strongly consistent in all scenarios — and can deliberately relax consistency under certain conditions.</p>
<p>I will explore this concept further in upcoming articles.</p>
]]></content:encoded></item><item><title><![CDATA[Exactly-Once Delivery is Impossible]]></title><description><![CDATA[Background
We discussed this in previous articles that when engineers first encounter distributed systems, they subconsciously carry an assumption from single-machine programming: if something happens, the system knows it happened. If it didn’t happe...]]></description><link>https://blog.suryasathi.com/exactly-once-delivery-is-impossible</link><guid isPermaLink="true">https://blog.suryasathi.com/exactly-once-delivery-is-impossible</guid><category><![CDATA[at-most-once]]></category><category><![CDATA[exactly-once]]></category><category><![CDATA[Exactly-Once Semantics]]></category><category><![CDATA[At-Least-Once]]></category><dc:creator><![CDATA[Surya Sathi]]></dc:creator><pubDate>Sun, 01 Feb 2026 18:30:40 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1769282701633/d2ee4914-f513-4aeb-a07b-a118e2690089.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3 id="heading-background">Background</h3>
<p>We discussed this in previous articles that when engineers first encounter distributed systems, they subconsciously carry an assumption from single-machine programming: if something happens, the system knows it happened. If it didn’t happen, the system knows that too. This assumption is rarely stated out loud, but it shows up everywhere—in how we design APIs, how we reason about failures, and how confident we are when we say things like “the request failed.”</p>
<p>We dismantled this assumption as well already. We saw that timeouts do not tell us what happened, only that we waited long enough to become uncomfortable. We saw that retries are not sloppy engineering, but a rational response to uncertainty. Once a system operates across processes, machines, or networks, it can no longer observe reality directly. It must act based on incomplete information.</p>
<p>Once this is accepted, a deeper question naturally follows: if systems cannot know with certainty whether an operation happened, how can they ever guarantee that it happened exactly once?</p>
<p>This is where the idea of <em>exactly-once delivery</em> enters the conversation. It is one of the most attractive promises in distributed systems, and can easily be one of the most misunderstood.</p>
<p>Exactly-once delivery sounds like the ideal world. A message is sent once, delivered once, processed once, and never duplicated. Nothing is lost, nothing is repeated, and downstream systems can stay simple. If such a guarantee were possible in the general case, an enormous amount of complexity would disappear. There would be no need for deduplication or idempotency. We could trust the infrastructure to “just handle it.”</p>
<p>It is therefore not surprising that many of us believe this should be achievable. After all, databases often feel exactly-once. Function calls feel exactly-once. Even some distributed systems documentation uses the phrase casually. But this intuition quietly assumes something that no distributed system can have: perfect knowledge of what is occurring.</p>
<p>To talk about this clearly, it helps to introduce the standard delivery semantics used in distributed systems: <strong>at-most-once</strong>, <strong>at-least-once</strong>, and <strong>exactly-once</strong>. These terms represent fundamentally different tradeoffs.</p>
<h3 id="heading-at-most-once-delivery">At-most-once delivery</h3>
<p>As the name suggests, it means a message will be delivered at most once or won’t be delivered at all. If something goes wrong, the system does not intentionally retry. The message may simply be lost. This model aligns closely with the simplest possible behavior: send a request, wait for a response, and if nothing arrives, move on. No retries, no tracking, no recovery.</p>
<p><strong><em>Note:</em></strong> <em>Technically, some retries by default can happen from clients or load balancers, but the system won’t retry intentionally.</em></p>
<p>The appeal of at-most-once delivery is that duplication is impossible. Because the system never retries, downstream logic never has to worry about processing the same message twice. There is no risk of double-charging a user or duplicating a record due to retries. The system is simple, fast, and predictable.</p>
<p>The cost of this simplicity is correctness. If a request reaches a server, is partially processed, and the response never makes it back to the caller, the system has no way to recover. At-most-once delivery assumes that occasional loss is acceptable. This is why it is better to be used for telemetry, metrics, logging, and other best-effort data. If a log line or metric point is dropped, no invariant is violated. The system still functions.</p>
<h3 id="heading-at-least-once-delivery">At-least-once delivery</h3>
<p>At-least-once delivery sits on the other side of the spectrum. Under this model, the system guarantees that a message will eventually be delivered, even if it has to be delivered multiple times. Loss is unacceptable, so retries are mandatory.</p>
<p>This model emerges naturally once uncertainty is acknowledged. As discussed in previous articles, if a system cannot know whether a message was processed successfully, and correctness matters, the only rational response is to retry. Over time, this guarantees delivery, but it introduces duplication. The same message may be processed twice, concurrently, or long after the original attempt.</p>
<p>Most real-world distributed systems choose at-least-once delivery precisely because it aligns with reality. Load balancers retry. Clients retry. Message queues retry. Even infrastructure you don’t control retries on your behalf.</p>
<p>At-least-once delivery preserves correctness in the face of failure, but only if the system is designed to tolerate repetition. Without additional safeguards, duplication leads to corrupted state: double charges, duplicate orders, repeated notifications, and inconsistent counters. This is why at-least-once delivery must always be paired with idempotent processing. <strong><em>Delivery is allowed to repeat; effects are not.</em></strong></p>
<h3 id="heading-exactly-once-delivery">Exactly-once delivery</h3>
<p>Exactly-once delivery appears, at first glance, to offer the best of both worlds: no loss and no duplication. The reason it is so tempting is that it seems like a small step beyond at-least-once. Add acknowledgements. Track offsets. Retry carefully. Surely, with enough bookkeeping, the problem can be solved.</p>
<p>The flaw in this reasoning is not in the bookkeeping, but in the assumption that acknowledgements represent truth.</p>
<blockquote>
<p>An acknowledgement is also a message.</p>
</blockquote>
<p>It can be delayed, duplicated, reordered, or lost in exactly the same ways as the original message. If a sender does not receive an acknowledgement, it cannot tell whether the message failed or succeeded. This is just another variant of the <em>two generals’ problem</em> we discussed previously. What matters is the consequence: retries are unavoidable, and retries imply possible duplication.</p>
<p>This leads to a fundamental conclusion that we need to eventually internalize: exactly-once delivery in the general case is impossible. Not difficult. <strong><em>Impossible</em></strong>. It would require perfect knowledge of events in an imperfect world.</p>
<p>This is why serious systems stop trying to guarantee delivery and instead focus on something more achievable: guaranteeing effects.</p>
<h3 id="heading-exactly-once-delivery-may-be-impossible-its-effects-are-not">Exactly-once delivery may be impossible; its effects are not</h3>
<p>Delivery is about movement. It is about messages traveling through space and time. Effects are about state. They are about whether money was charged, whether a record was created, whether a balance was updated. Delivery can be duplicated. Effects must not be allowed to accumulate incorrectly.</p>
<p>Once this distinction is made, the design goal shifts. The system no longer tries to prevent messages from arriving more than once. Instead, it ensures that processing the same message multiple times produces the same final state. Repetition is no longer an error; it is a condition to be handled.</p>
<p>This is what we discussed about in idempotency. What idempotency means is exactly-once <em>effects</em>. A message might be delivered ten times. The system ensures that the business outcome occurs once.</p>
<p>This reframing explains why many products have, at various points, claimed to offer exactly-once guarantees without actually contradicting reality.</p>
<h3 id="heading-how-products-promise-exactly-once-delivery">How products promise exactly-once delivery</h3>
<p>Kafka’s “exactly-once semantics” is a good example. Kafka does not guarantee exactly-once delivery across arbitrary consumers, failures, and side effects. What it guarantees is exactly-once <em>processing</em> within a carefully defined boundary.</p>
<p>In Kafka’s case, that boundary is a single Kafka cluster with transactional producers and consumers, where state changes are coordinated with offset commits. A consumer processes records, updates its state, and commits offsets as part of a single transaction. If the consumer crashes before the transaction commits, the transaction is aborted and the records are replayed. If the transaction commits, both the state update and the offset commit become visible together.</p>
<blockquote>
<p>Within this boundary, effects appear exactly once.</p>
</blockquote>
<p>The hidden condition is crucial: all relevant state must participate in the same transactional system. As soon as side effects escape that boundary—sending an email, calling an external API, charging a credit card—the guarantee no longer applies. So, Kafka did not solve exactly-once delivery in the open world. It solved exactly-once effects inside a controlled domain with a trusted coordinator.</p>
<p>Databases make similar promises. When a database executes a unique transaction, it guarantees that the transaction’s effects are applied once, even if the client retries after a timeout. This works not because the request arrived once, but because the database enforces atomicity and uniqueness. The database is the authority that decides whether a state change is allowed to occur.</p>
<h3 id="heading-conclusion">Conclusion</h3>
<p>In every real system that claims exactly-once behavior, this same pattern appears. The guarantee is never about delivery itself. It is about effects within a boundary, enforced by a system that has the authority to reject duplicates and serialize conflicts. Once you leave that boundary, the guarantee dissolves.</p>
<p>This is why one needs to be skeptical of broad exactly-once claims. Without precise definitions, the promise is usually illusory.</p>
<p>This conclusion is not pessimistic. It is clarifying. Exactly-once delivery is the wrong goal. Exactly-once effects are achievable, practical, and sufficient. Systems that embrace this reality are easier to recover, easier to reason about, and more honest about their limitations.</p>
<p>Now, if delivery cannot be trusted, then correctness must come from somewhere else. Understanding how systems decide on correctness, requires talking about consistency and authority. That is where we go next.</p>
]]></content:encoded></item><item><title><![CDATA[Idempotency]]></title><description><![CDATA[When you read about idempotency the first time, it sounds reasonably precise and simple. The definition is short: performing the same operation multiple times should have the same effect as performing it once. On paper, it feels like a solid guarante...]]></description><link>https://blog.suryasathi.com/idempotency</link><guid isPermaLink="true">https://blog.suryasathi.com/idempotency</guid><category><![CDATA[idempotence]]></category><category><![CDATA[idempotency]]></category><category><![CDATA[consistency]]></category><category><![CDATA[deduplication]]></category><dc:creator><![CDATA[Surya Sathi]]></dc:creator><pubDate>Mon, 26 Jan 2026 06:30:14 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1768128098696/8c3c00e5-fb51-4205-9036-addd40969989.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When you read about idempotency the first time, it sounds reasonably precise and simple. The definition is short: performing the same operation multiple times should have the same effect as performing it once. On paper, it feels like a solid guarantee. If something can be retried safely, then retries stop being scary.</p>
<p>But the more you try to reason through real scenarios, the harder it becomes to hold that definition true. Idempotency only describes a property a system should have — not the conditions, constraints, or consequences required to actually maintain it.</p>
<p>To understand why, it helps to start with a…</p>
<h2 id="heading-very-ordinary-situation">Very ordinary situation</h2>
<p>Assume a user added some pizzas in their food delivery app and on the checkout screen, they clicked pay. Now the client, the user’s mobile app, waits for a response — 30 seconds, 1 minute, 2 minutes — but nothing comes back. Maybe the server was slow. Maybe the response was lost. From the client’s point of view, there’s no reliable way to know what happened. The request might have failed, or it might have succeeded and the response just never arrived.</p>
<p>At this point, retrying is not a design choice but a necessity. Waiting forever isn’t acceptable, and assuming failure is risky. So the client retries the request.</p>
<p>Now imagine you’re responsible for handling that request on the server. The obvious concern is duplication. You don’t want the same user to be charged twice. A natural first idea is to check whether the action has already been performed. If it has, do nothing. If it hasn’t, proceed.</p>
<p>That logic feels careful. It also aligns nicely with the intuitive definition of idempotency. If the operation already happened, don’t repeat it.</p>
<p>You will see if two requests have same content like same pizzas from the same customer — if so, mark it as a duplicate and ignore it. But what if the customer deliberately ordered the same pizza two times, they may have ordered lesser amount the first and ordered again - or even maybe, one of the user’s friends friends also uses the same account and ordered from their device again.</p>
<p>So checking the request content can’t be a reliable way. What if you check the device IP or some other property to check if it’s a duplicate. You can hash the user id, device IP and send it in the headers and check on your server. But sending the IP and storing it on your servers to check may not be a very best practice.</p>
<p>So you decide to just generate a UUID and send that in the headers and on your server you store it in a cache and whenever a request arrives, you will first check your cache to see if the request was already received. And you call it…</p>
<h2 id="heading-an-idempotency-key">An Idempotency-Key</h2>
<p>If requests were processed strictly one at a time, this approach would be enough. The first request would cache the key and create the payment. Any retry arriving afterward would see that the the key exists and exit early.</p>
<p>But you are clever enough to know that it’s not practical. You do horizontal scaling and storing state in a server locally is a bad idea so you use a Redis cluster. Now even if the request first lands on an unfortunately slow server and the retry falls on a faster one, the second one will first check the Redis and know that the request was already received and return a generic response, say, <em>“your request is being processed”</em>.</p>
<p>But then you recall that that your Redis cluster prioritizes availability and performance and only offers asynchronous consistency. And you immediately recognize that it is a problem here.</p>
<p>What if the second server checked for the key in a Redis node that hasn’t been updated yet due to a lag in replication?</p>
<p>Forget Redis, even in the case of a strongly consistent database, if a request and its retry was processed concurrently at the same time, if you first check and then make an entry for idempotency key, without it being an atomic transaction, you are risking duplication.</p>
<p>Both your servers think the system received the request for the first time, both assume it’s safe to proceed and both start processing the request.</p>
<h2 id="heading-check-then-act-no-longer-works">Check-then-act no longer works</h2>
<p>Our “check then act” logic no longer works when both the request are processed parallelly.</p>
<p>Even if the cache was replicated almost instantly, just in case the retry arrives an hour later and by that time the cache was already cleared, the retry succeeds the idempotency key check and <em>can be</em> processed again.</p>
<p>Nothing exotic had to happen for this to happen. There was no bug in the code. There was no unexpected input. The failure came, purely, from assuming that the state observed during the check would be consistent enough or even long enough for the action to be safe.</p>
<p>The logic was correct in isolation, but unsafe in the presence of concurrency and network delays.</p>
<p>In this setup, ‘check before I act’ stopped being enough. Idempotency has to mean something stronger. So…</p>
<h2 id="heading-what-exactly-is-idempotency">What exactly is idempotency?</h2>
<p>A more useful way to think about idempotency is in terms of final state. An operation is idempotent if executing it multiple times — whether sequentially or concurrently — leads to the same final state as executing it once. The focus shifts away from how many times the code runs and towards what the system looks like afterward.</p>
<p>Once you think in terms of state, another complication becomes obvious. The system’s state is not limited to database rows. Sending an email changes the world. Sending an SMS changes the world. Calling an external payment provider changes the world. These effects often live outside transactional boundaries and cannot be undone.</p>
<p>A system might successfully prevent duplicate database records and still send two emails. From the database’s perspective, everything is consistent. From a user’s perspective, something is clearly wrong. This makes it clear that idempotency can’t stop at data storage. It has to account for side effects as well.</p>
<p>Now this doesn’t negate the use of idempotency keys. While idempotency keys can recognize duplicates, they aren’t the complete solutions either.</p>
<p>An idempotency key can only protect what is gated behind it. If some side effects happen outside that protection, retries can still cause duplication in places that matter.</p>
<p>At this point, a pattern becomes hard to ignore. A fragile solution relies on checking state and hoping it doesn’t change at the wrong moment. A robust solution seems to require something stronger — a way to ensure that certain states cannot exist at the same time, no matter how requests interleave. So, is there…</p>
<h2 id="heading-a-potential-solution">A potential solution?</h2>
<p>One pattern I could find, that seemed to handle this better, is gating every sensitive action to prevent duplicates. We can do our check-then-act at the first boundary, but then even in the subsequent parts of the system, we ensure that duplication is not possible.</p>
<p>For example, in this case, generating an order id even before the request hits the server or generating an order id consistently for a request and its retries somehow — so a request and its retry must have the same order id and when both try to create a record in a strongly consistent database, one of them will inevitably fail due to a collision in the order id.</p>
<p>Depending on the system, this may or may not be worth the complexity. You may need your own tailor-made solution.</p>
<p>But wait…</p>
<p>What if the instance that is processing the request at a later part of the process crashes? The retries won’t be triggering the request again and the request has already failed. So instead what we can do is, store the progress and position along with the key.</p>
<h2 id="heading-a-retry-should-recheck-not-re-execute">A retry should recheck; not re-execute</h2>
<p>Instead of treating a request as a single, all-or-nothing action, we should start treating it as a process with memory.</p>
<p>The idempotency key can no longer represent just “this request has been seen before.” It should represent where the system currently is with respect to that request.</p>
<p>When the request arrives for the first time, the system creates an entry associated with the idempotency key. But instead of only recording that the request exists, it also records progress. A status. A position. A checkpoint.</p>
<p>Something like:</p>
<ul>
<li><p>received</p>
</li>
<li><p>payment initiated</p>
</li>
<li><p>payment confirmed</p>
</li>
<li><p>order created</p>
</li>
<li><p>notification sent</p>
</li>
</ul>
<p>The exact steps don’t matter. What matters is that the system now has a way to answer a much more useful question than “have I seen this request?”</p>
<p>It can answer: “How far did I get last time?”</p>
<p>The server successfully initiates the payment, but crashes before sending a response. The client retries. The retry carries the same idempotency key. This time, instead of blindly starting from the beginning or blindly rejecting the request, the system looks up the key and sees that the process is already partway through.</p>
<p>If payment was already initiated, the system doesn’t initiate it again.<br />If the order record was already created, it doesn’t create another one.<br />If a notification was already sent, it doesn’t send it again.</p>
<p>Retries should not be treated as duplicates that must be blocked at the gate but as re-entries into a workflow. The same key should lead to the same execution path, starting from wherever the system last stopped.</p>
<p>Yes, this is difficult. This means different parts of your system, <em>potentially</em> handled by different teams, must be in sync about how they are handling idempotency. But this may be needed for the problem you are solving.</p>
<p>This is where a subtle but important distinction appears.</p>
<p>Idempotency is not about preventing work from happening more than once. It’s about preventing observable effects from happening more than once.</p>
<p>The system may execute code multiple times. It may retry steps. It may re-run logic after crashes. None of that violates idempotency as long as the outside world cannot observe duplicated outcomes.</p>
<p>“Performing the same operation multiple times has the same effect as performing it once” is technically correct — but only if we’re clear about what <em>effect</em> means. It doesn’t mean “the code ran once.” It means “the world ended up in the same state.”</p>
<h2 id="heading-an-important-observation">An important observation</h2>
<p>At this point, something uncomfortable becomes hard to ignore.</p>
<p>If idempotency requires memory, progress tracking, guarded side effects, and careful coordination across boundaries, then it’s no longer just a retry optimization. It’s a systemic property — one that depends on how work is claimed, how progress is recorded, and how effects are exposed to the outside world.</p>
<p>And that raises a deeper question.</p>
<p>If we have to tolerate retries, crashes, partial execution, and re-entries at every step — how can you be sure that something happened <em>exactly once</em>?</p>
<p>That’s a discussion for another article.</p>
]]></content:encoded></item><item><title><![CDATA[Why Distributed Systems Can't Know What's Happening]]></title><description><![CDATA[Background
When we write programs, we have a quiet assumption that goes unnoticed until it gets explicitly questioned — when you send a request, it either succeeds or fails; even when you query a database, it either succeeds or fails. It always holds...]]></description><link>https://blog.suryasathi.com/why-distributed-systems-cant-know-whats-happening</link><guid isPermaLink="true">https://blog.suryasathi.com/why-distributed-systems-cant-know-whats-happening</guid><category><![CDATA[distributed system]]></category><category><![CDATA[retries]]></category><category><![CDATA[timeout]]></category><category><![CDATA[Uncertainty]]></category><category><![CDATA[side-effects]]></category><category><![CDATA[Correctness]]></category><dc:creator><![CDATA[Surya Sathi]]></dc:creator><pubDate>Mon, 19 Jan 2026 06:30:54 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1767458422414/e9920d7f-1010-4710-b67f-04fce2fe6ff8.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-background">Background</h2>
<p>When we write programs, we have a quiet assumption that goes unnoticed until it gets explicitly questioned — when you send a request, it either succeeds or fails; even when you query a database, it either succeeds or fails. It always holds true when you are developing programs locally — because a local function either returns a value or throws an error, your local database either connects and queries or throws an error, your local API call reaches your program or fails.</p>
<p>Nobody can notice this on local runs because everything runs on same machine — your program, your database, your cache etc. There is no ambiguity about whether an operation ran or not. The program is the authority on its own actions. You get immediate feedback.</p>
<p>The moment network enters the picture, it no longer holds true — your program runs somewhere, database runs somewhere, cache runs somewhere. They communicate through network calls.</p>
<h2 id="heading-two-generals-problem">Two Generals Problem</h2>
<p>There is a well-known thought experiment in distributed systems called the Two Generals Problem. It describes two armies positioned on opposite hills. They want to coordinate an attack, but the only way they can communicate is by sending messengers through a valley where their common enemy’s establishment is positioned. The only way they can communicate is by sending messages, that have to travel through the valley. If first general sends a message proposing an attack time, the message can be captured in the valley by the enemy — so the second general does not receive the message. Or if the second general receives the message and send an acknowledgement, which again has to pass through the valley — it can again get captured by the enemy — So the first general does not know if their message was delivered at all. So the first general sends another message for confirmation. Now the problem repeats — that confirmation might also be lost. And so on. Either ways, there is no way both the generals can know whether their messages were delivered or not.</p>
<p>The core insight of the problem is not tactical or military. It is about certainty. No matter how many messages are exchanged, neither general can ever be completely certain that the other side knows what it knows. At some point, a decision must be made without certainty.</p>
<p>Now, the generals are two systems and the valley is the network. This is not a hypothetical edge case or a missing protocol. It is a fundamental limitation of systems that communicate through unreliable channels. Once messages can be delayed or dropped, certainty becomes impossible.</p>
<p>Every networked system lives inside this constraint.</p>
<p>The Two Generals Problem does not tell us how to fix this situation. It tells us that it cannot be fixed.</p>
<blockquote>
<p>Systems that communicate over networks must operate under permanent uncertainty.</p>
</blockquote>
<h2 id="heading-latency-is-uncertainty">Latency is uncertainty</h2>
<p>Different components of a distributed system communicate through messages; but when a program sends a message to another program asking it to do something, certainty disappears. The sender no longer has a direct visibility into what happened on the other side. It can only wait for a response, and waiting introduces latency.</p>
<blockquote>
<p>Latency, in distributed systems, is not just delay. It is uncertainty.</p>
</blockquote>
<p>The issue is not that responses are slow; but that client does not know whether it is “slow” or “never.” When a request is sent across a network, silence does not mean failure, but also not success.</p>
<blockquote>
<p>Silence simply means the system does not know.</p>
</blockquote>
<h2 id="heading-timeout-is-a-guess">Timeout is a guess</h2>
<p><strong>Timeouts</strong> are how systems cope with this uncertainty — you mark a request failed, if it didn’t respond in time — but timeouts are not facts, they are guesses. A timeout does not tell you that an operation failed; it tells you that you waited long enough.</p>
<p>From the client’s perspective, why it didn’t receive response doesn’t matter — it just didn’t receive the response in time, so it thinks the request might have failed; that’s the only thing that matters to it.</p>
<h2 id="heading-logs-are-leading-and-misleading">Logs are leading and misleading</h2>
<p>If a system can sometimes not know what happened, partial failures stop being edge cases and starts looking like default conditions.</p>
<p>A system can be half broken in ways that are invisible from any single point of view. A request can reach the server, be processed fully, and commit changes to the database, while the response packet is dropped on the way back to the client. Or processing happened in time but the response might arrive just a millisecond later. But that doesn’t matter to either of the components.</p>
<p>From the server’s logs, the request succeeded. From the client’s logs, it failed. Both logs are accurate. Neither tells the whole story.</p>
<p>This is why logs feel leading and misleading at the same time. Logs from a single process describe what it believes happened. They do not describe what actually happened globally, because they don’t have global visibility. In a distributed system, every component tells a local story, and those stories can contradict each other without any of them being wrong.</p>
<h3 id="heading-important-note">Important Note:</h3>
<p>This is where observability and tracing come into the picture; to observe the lifecycle of a request and get the whole picture — but that’s a story for another time.</p>
<p>Also, while global logs might give you the visibility of what happened to a request — it is — after the fact; you notice an issue only when the problem has already started or a user has experienced it; not during decision time. It cannot help prevent problems, just diagnose what happened.</p>
<h2 id="heading-forced-to-decide-in-uncertainty">Forced to decide in uncertainty</h2>
<p>Can’t we just wait longer? No. How do you decide how long a program should wait?</p>
<p>Systems cannot wait forever. Users are on the other side of these systems, and users have expectations. A user clicking a button expects something to happen within seconds, not minutes.</p>
<blockquote>
<p>These expectations force systems to make decisions while living in uncertainty.</p>
</blockquote>
<p>So distributed systems are not allowed to wait until they know the truth. They must act while still uncertain. They must decide based on incomplete information.</p>
<p>It is a fundamental constraint of systems that communicate over networks. Once messages can be delayed, reordered, or dropped, certainty disappears. The system must move forward anyway.</p>
<h2 id="heading-retries-are-not-optional">Retries are not optional</h2>
<p>Users expect progress. Products promise availability. When uncertainty appears, the system must choose an action, and the most common action it chooses is to try again.</p>
<p>Retries exist not because engineers love complexity, but because users hate waiting. A slow system feels broken even when it is technically correct. From a user’s perspective, a retry that succeeds is indistinguishable from a system that worked the first time. From a technical perspective, retries smooth over temporary failures.</p>
<p>What makes retries especially dangerous is that they rarely belong to a single place in the system. Even if you decide not to retry in your applications, something else almost certainly will. Browsers retry requests when connections are dropped. SDKs retry when you switch networks. Load balancers retry when upstream appears slow. Message queues redeliver messages when acknowledgements are delayed.</p>
<p>So you cannot fully control retries. You should design for them whether you like it or not.</p>
<h2 id="heading-a-retry-is-not-a-repeat-it-is-a-correctness-problem">A retry is not a repeat; it is a correctness problem</h2>
<p>One would often think of a retry as a repeat of the same action, as if the system tried again under identical conditions. That is not what a retry is. A retry is not a replay. It is a new attempt after some time, under different load, possibly on a different machine, with different activity surrounding it.</p>
<p>Between the original request and the retry, the state may have changed. Caches may have warmed or expired. Locks may have been acquired or released. Other requests may have modified shared state.</p>
<p>This turns retries into a correctness problem.</p>
<p>Once retries exist, the same logical request can arrive more than once. It can arrive twice in sequence because the client timed out. It can arrive concurrently because a load balancer retried while the original request was still executing. It can arrive after partial success because the server crashed after committing data but before responding. It can even arrive out of order, with a later retry processed before the earlier attempt finishes.</p>
<p>From the system’s point of view, these are not edge cases. They are normal conditions.</p>
<h2 id="heading-side-effects">Side effects</h2>
<p>Up to this point, it is easy to think of a request as something that “runs code” and produces a result. But systems don’t just compute values. They change the reality around them. These changes are what we call side effects.</p>
<p>The moment an action changes something outside the process that executed it, you’ve created a side effect. Writing to a database is a side effect because it changes shared state. Publishing a message to another service is a side effect because it causes the other program to do some work. Sending an email is a side effect. Charging a credit card is a side effect.</p>
<h2 id="heading-effect-of-retries-on-state-and-side-effects">Effect of retries on state and side effects</h2>
<p>The moment a system processes the same request more than once, state becomes vulnerable. Data that was meant to be written once may be written twice. Counters may increment incorrectly. Emails may be sent twice. Money may be charged twice. Workflows may be triggered twice.</p>
<p>The code often reads as if a request has a clear beginning, a clear end, and a single effect on the world. Retries break that narrative. They turn one logical action into multiple physical executions, each capable of producing side effects.</p>
<p>What makes side effects different from state is that they are not reversible by default. State can sometimes be corrected. Side effects often cannot.</p>
<blockquote>
<p>State lives <em>inside</em> the system and can sometimes be corrected.</p>
<p>Side effects leak <em>outside</em> the system and require compensation, not correction.</p>
</blockquote>
<p>If a database row is written incorrectly, it can be updated later. But many side effects cannot be undone cleanly. An email cannot be unread. An API cannot be uncalled. A payment cannot simply be “taken back” without a compensating action that introduces even more complexity.</p>
<blockquote>
<p>The cost of duplication is not evenly distributed across actions.</p>
</blockquote>
<p>A retry does not just risk doing the same work twice. It risks affecting the world twice.</p>
<p>So, it becomes impossible to dismiss duplicates as rare edge cases. If retries are inevitable, and retries create multiple execution attempts, then duplicate processing is not an accident. It becomes a property of the system.</p>
<h2 id="heading-correctness-must-be-a-property-of-the-system">Correctness must be a property of the system</h2>
<p>Correctness is no longer about success in a single execution. It’s about not producing redundant side effects under repeated execution. That is a much harder guarantee to provide, and it cannot be achieved by hoping retries won’t happen.</p>
<p>Once duplicates are guaranteed, correctness must be <strong><em>designed</em></strong>. It cannot be assumed and cannot be bolted on with logging or monitoring. It must be a property of the system, just like latency or throughput.</p>
<p>Now the real question is no longer whether an operation ran but whether running it more than once is safe.</p>
<p>This is where idempotency becomes unavoidable.</p>
]]></content:encoded></item><item><title><![CDATA[Caching: Performance vs Consistency]]></title><description><![CDATA[Every computing system, no matter how low level or high level, is constrained by the same fundamental problem: fetching data takes time. Computation itself is fast, but fetching data is slow.
A modern CPU is extraordinarily fast. But what slows a sys...]]></description><link>https://blog.suryasathi.com/caching-performance-vs-consistency</link><guid isPermaLink="true">https://blog.suryasathi.com/caching-performance-vs-consistency</guid><category><![CDATA[caching]]></category><category><![CDATA[cache]]></category><category><![CDATA[CDN]]></category><category><![CDATA[TTL]]></category><category><![CDATA[Cache Invalidation]]></category><dc:creator><![CDATA[Surya Sathi]]></dc:creator><pubDate>Mon, 12 Jan 2026 06:30:49 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1767457829878/71e1fdc4-2acb-4571-9fad-84f1f8622ca0.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Every computing system, no matter how low level or high level, is constrained by the same fundamental problem: <strong>fetching data takes time</strong>. Computation itself is fast, but fetching data is slow.</p>
<p>A modern CPU is extraordinarily fast. But what slows a system down is not only the processing speed but the access to data. How fast can a system get the data it required to perform a computation?</p>
<p>Any computation that is involved with a state, must fetch the state from somewhere. Be it from Register/L1/L2/L3 cache at the CPU level, be it from memory (RAM) at code level, be it from disk (SSD/HDD) at OS level or even be it from network at higher levels.</p>
<p>The further you go from the place the data is needed, the longer you wait, the higher the latency is and the lower the performance is.</p>
<p>A way around this is to store copies of most frequently accessed/predicted to be accessed data, as near as possible. This is what’s called <strong>a cache.</strong></p>
<p>If you need to know why cache is so important, you need to have a look at the physical reality of how long it takes to access data:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Storage Layer</strong></td><td><strong>Approximate Latency</strong></td></tr>
</thead>
<tbody>
<tr>
<td>CPU Register</td><td>~0.3 ns</td></tr>
<tr>
<td>L1 Cache</td><td>~1 ns</td></tr>
<tr>
<td>L2 Cache</td><td>~3–5 ns</td></tr>
<tr>
<td>L3 Cache</td><td>~10–20 ns</td></tr>
<tr>
<td>RAM</td><td>~80–120 ns</td></tr>
<tr>
<td>SSD</td><td>~100 µs</td></tr>
<tr>
<td>HDD</td><td>~5–10 ms</td></tr>
<tr>
<td>Network</td><td>1–100+ ms</td></tr>
</tbody>
</table>
</div><p>Accessing RAM is roughly a hundred times slower than accessing L1 cache. Accessing an SSD is much slower than RAM. A network call is millions of times slower than a CPU instruction.</p>
<p>For most real-world workloads, memory and I/O latency dominates computation time. A modern CPU can execute millions of instructions in the time it takes to fetch data from across the network.</p>
<p>That is why caches exist.</p>
<h2 id="heading-why-cpus-cache-at-all">Why CPUs cache at all?</h2>
<p>If a CPU had to fetch every piece of data directly from RAM, most of its time would be spent idle. To avoid this, CPUs keep caches. These caches work because real programs exhibit patterns.</p>
<p>Programs tend to reuse the same data repeatedly, which is known as temporal locality. For example same variable in a code being accessed repeatedly.</p>
<p>They also tend to access data near other recently accessed data, known as spatial locality. When a CPU reads memory, it does not fetch a single byte — it fetches a whole block, predicting that nearby data will be needed soon as per spatial locality.</p>
<p>As programs grew more complex, a single cache was not enough. Multiple layers emerged: L1 closest to the core, L2 slightly farther, and L3 shared across cores. Each level is larger and slower than the previous one. This hierarchy exists for one reason only: to keep frequently used data as close to computation as possible.</p>
<p>This idea does not stop at the CPU.</p>
<h2 id="heading-the-same-problem-appears-in-software-systems">The same problem appears in software systems</h2>
<p>Once you move beyond a single machine, the same pattern appears at a larger scale. Instead of CPU trying to read from RAM, you now have applications trying to read from databases. Instead of memory access, you have disk reads and network calls.</p>
<p>A request that crosses a network, a database query that scans a disk is orders of magnitude slower than a computation done in memory.</p>
<p>So the same idea reappears: keep frequently used data closer to where it is needed.</p>
<p>Databases cache index pages in memory so they don’t have to reread them from disk. Operating systems cache file blocks. Applications cache computed results. Reverse proxies cache HTTP responses. CDNs cache content close to users.</p>
<p>Each of these is the same idea expressed at a different level.</p>
<h2 id="heading-caching-is-inherently-risky-for-consistency">Caching is inherently risky for consistency</h2>
<p>Caching literally means creating a copy of data. Which means, you are inherently taking a risk that it may be stale — as the original source might get updated but the cache might not be, unless explicitly done.</p>
<p><strong>Cache, ideally, should not be the source of truth.</strong></p>
<p>This distinction is crucial. To understand this better, consider what happens when data changes.</p>
<p>A request comes in and reads data from a database. The result is cached somewhere. Later, another request modifies the data in the database — but cache is not updated at the same time. The cached copy is now wrong. Nothing breaks immediately, but the system is now inconsistent.</p>
<p>Caches are <em>often</em> eventually consistent unless designed to provide stronger guarantees. That is, given enough time and no further updates, the stale data will eventually be replaced or discarded.</p>
<p>A quick note: Based on the strategy used, caching can be eventually or strongly consistent, as you will see soon.</p>
<p>Now a cache doesn’t magically fix itself with time. Somehow, the old data has to be discarded so that new requests will hit the source of truth directly, or it has to be replaced with the newer results. How that happens depends on the caching strategy you choose.</p>
<h2 id="heading-how-cache-eventually-becomes-consistent">How cache eventually becomes consistent</h2>
<h3 id="heading-ttl">TTL</h3>
<p>The simplest mechanism is time. Cached data is stored with a time limit, called a Time-To-Live (TTL). Once that time expires, the entry is discarded. The next request fetches fresh data from the source and repopulates the cache.</p>
<p>This is simple but effective. The system risks a known window of inconsistency in exchange for speed. The TTL can vary with use case like 60s, 15m, 1h so on.</p>
<p>Here caches are <em>eventually</em> consistent, because <strong>stale data eventually expires once you keep a finite TTL</strong>.</p>
<h3 id="heading-explicit-invalidation">Explicit Invalidation</h3>
<p>More sophisticated systems try to be smarter. When data is updated, the cache entry may be explicitly removed or replaced. This requires the system to know exactly which cache entries are affected by each write.</p>
<p>For example, when a user updates their profile, the service writes to the database and also deletes or updates the cache entry</p>
<p>This reduces staleness, but introduces complexity.</p>
<p>Because now the system must know which cache keys are affected and update or invalidate them reliably and also handle failures, if any, during invalidation. If invalidation fails, stale data persists.</p>
<h2 id="heading-eviction-policies">Eviction Policies</h2>
<p>When the cache is full, you will have to decide what to keep and what to decide. This is where eviction policies come into the picture. While TTL and invalidation decide <em>when data becomes invalid</em>, eviction policies decide <em>what to remove</em> when cache is full</p>
<h3 id="heading-lru">LRU</h3>
<p><strong>Least Recently Used</strong> is a popular strategy that discards items that haven’t been used recently starting from the least recently accessed ones.</p>
<h3 id="heading-lfu">LFU</h3>
<p>In <strong>Least Frequently Used</strong>, instead of least recently accessed, you take out least frequently accessed ones.</p>
<h3 id="heading-fifo">FIFO</h3>
<p>This is <strong>First-In-First-Out.</strong> That is, whichever is the oldest cached data, will get evicted.</p>
<h2 id="heading-caching-strategies">Caching Strategies</h2>
<p>There are different ways one can choose to cache.</p>
<h3 id="heading-cache-aside">Cache-Aside</h3>
<p>This is the the most commonly used strategy.</p>
<pre><code class="lang-mermaid">flowchart LR
    R((Request))
    S[Service]
    C[Cache]
    DB[Database]
    R --&gt; S --&gt; |1 - Check in cache| C
    S --&gt; |2 - Not found in cache, query DB| DB --&gt; |3 - Response from DB| S
    S --&gt; |4 - Cache the response| C
</code></pre>
<h3 id="heading-read-through-cache">Read-Through Cache</h3>
<p>Here, the service talk only with cache, never directly with DB. That’s why it’s called read-through cache. But the problem is, cache becomes a critical part of the path here.</p>
<p>A real-world use case can be found here: <a target="_blank" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DAX.concepts.html#:~:text=Read%20operations,the%20primary%20node.">AWS DynamoDB uses DAX, which serves results from cache - if not found, it fetches from DynamoDB and returns the result, as well as store it in cache for future use.</a></p>
<pre><code class="lang-mermaid">flowchart LR
    R((Request))
    S[Service]
    C[Cache]
    DB[Database]
    R --&gt; S --&gt; C --&gt; S
    C --&gt; |Not found in cache, query DB| DB
</code></pre>
<h3 id="heading-write-through-cache">Write-Through Cache</h3>
<p>Here, writes are done to cache first and the cache becomes responsible for forwarding the write to DB. Since the cache is now part of the write path, its availability directly affects system correctness.</p>
<pre><code class="lang-mermaid">flowchart LR
    R((Write))
    S[Service]
    C[Cache]
    R --&gt; S --&gt; C --&gt; DB
</code></pre>
<h3 id="heading-write-behind-cache">Write-Behind Cache</h3>
<p>Here, writes are done to cache only initially. DB will be updated later i.e., asynchronously. This serves extremely fast writes but comes with a serious tradeoff: if the process crashes before the database update completes, data can be lost. This trades durability for throughput.</p>
<pre><code class="lang-mermaid">flowchart TD
    subgraph Immediate
        direction LR
        R((Write))
        S[Service]
        C[Cache]
        R --&gt; S --&gt; C
    end

    subgraph Asynchronous
        W[Worker]
        DB[Database]
        W --&gt; C
        W --&gt; DB
    end
</code></pre>
<h3 id="heading-refresh-ahead">Refresh-Ahead</h3>
<p>Cache serves stale data while refreshing in background before TTL expires. This is used by CDNs generally. This helps avoid cache stampedes, where once a cache item’s TTL expires, many requests simultaneously hit the DB, making it do the same query repeatedly for all those requests before the response is cached again.</p>
<h3 id="heading-tradeoffs">Tradeoffs</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Choice</strong></td><td><strong>Benefit</strong></td><td><strong>Cost</strong></td></tr>
</thead>
<tbody>
<tr>
<td>Long TTL</td><td>Very fast reads</td><td>Stale data</td></tr>
<tr>
<td>Short TTL</td><td>Fresher data</td><td>More DB load</td></tr>
<tr>
<td>Cache-Aside</td><td>Simple and <em>mostly</em> fault-tolerant</td><td>Can cause cache stampedes and stale reads if invalidation fails</td></tr>
<tr>
<td>Read-Through</td><td>Low latency</td><td>Cache becomes a critical component and consistency depends on write strategy</td></tr>
<tr>
<td>Write-Through</td><td>Strong consistency</td><td>Higher latency and cache becomes a critical component</td></tr>
<tr>
<td>Write-Behind</td><td>High throughput and low latency</td><td>Risk of data loss</td></tr>
<tr>
<td>Refresh-Ahead</td><td>Helps avoid cache stampedes</td><td>Increased complexity with adding background workers and risk of refreshing unnecessary data</td></tr>
</tbody>
</table>
</div><h2 id="heading-why-systems-slow-down-after-restarts">Why systems slow down after restarts</h2>
<p>When a cache is empty, it is called <strong>cold</strong>. Every request must go all the way to the database or backend service. Latency is high, and load spikes sharply.</p>
<p>As requests flow through the system, popular data accumulates in the cache. Over time, the cache becomes <strong>warm</strong>. Requests are served quickly, load drops, and the system stabilizes.</p>
<p>This is why systems often feel slow immediately after deployment or restart. The cache has no memory yet. Large systems often pre-fill caches with known hot data to avoid this cold-start problem. Others accept the temporary slowdown.</p>
<h2 id="heading-how-to-use-cache-effectively">How to use cache effectively?</h2>
<p>If a cache refreshes too aggressively, performance collapses. If it holds data too long or invalidation fails, you see outdated results. If many requests miss the cache at once, cache stampede occurs. Due to fast responses from cache, you might not even recognize poor database queries — which will become an issue under cache stampedes.</p>
<p>Which is why, you must design the system to work as efficiently as possible, <em>without</em> cache in mind, and add cache only as an additional layer for better performance.</p>
<p>Anything that requires strong correctness — financial balances, authorization, inventory counts — must not be cached. You can sacrifice performance for consistency here. Anything that can tolerate inconsistency — like home page feed, user profiles — can be cached safely.</p>
<p>A piece of advice I found: one must not start by thinking about what to cache but, by pointing out what not to cache.</p>
<p>If you add cache, your system should be faster but still be correct where it matters — don’t cache what must not be cached. And if you remove cache, your system may get slow but should still be correct without any breaks — don’t rely on cache too much.</p>
<h2 id="heading-cdn-caching-at-the-edge">CDN: Caching at the Edge</h2>
<p>Until now, we’ve talked about caching within a system. But the same latency problem exists at a global scale. When users are geographically far from servers, network latency dominates everything else.</p>
<p>For example, a simple website might be hosted in the US, but it might have many users in India. Now, if every request for the website goes across continents to the system in US, it will cause a lot of latency. So Content Delivery Networks (CDNs) cache static assets like HTML, CSS, JS and media files, near the edge i.e., near the users, to deliver them faster.</p>
<p>A CDN reduces latency by reducing the physical distance. But you will need to invalidate the cache actively, if you want any changes to appear immediately.</p>
<h3 id="heading-tradeoffs-1">Tradeoffs</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Benefit</strong></td><td><strong>Cost</strong></td></tr>
</thead>
<tbody>
<tr>
<td>Low latency</td><td>Eventual consistency</td></tr>
<tr>
<td>Lower load on origin server</td><td>Hard invalidation</td></tr>
<tr>
<td>High availability</td><td>Less control (as you typically don’t control the CDN)</td></tr>
<tr>
<td>Global scale</td><td>Debugging complexity (as you typically don’t control the CDN)</td></tr>
</tbody>
</table>
</div><p>A simple example of the risk is Cloudflare’s CDN outages that occurred recently (2022, 2025) — you can’t be confident if the issue is with your site or the CDN until the it is identified as a CDN incident.</p>
]]></content:encoded></item><item><title><![CDATA[State in a System]]></title><description><![CDATA[Assume you are making a very simple login system. A user sends a request with a username and password. Let us list down what all needs to be present in the system.

If the credentials are valid, a session should be created.

All the authorized action...]]></description><link>https://blog.suryasathi.com/state-in-a-system</link><guid isPermaLink="true">https://blog.suryasathi.com/state-in-a-system</guid><category><![CDATA[State Management ]]></category><category><![CDATA[idempotency]]></category><category><![CDATA[idempotence]]></category><category><![CDATA[locking]]></category><category><![CDATA[transactions]]></category><category><![CDATA[StateLESS]]></category><category><![CDATA[strong-consistency]]></category><category><![CDATA[eventual consistency]]></category><dc:creator><![CDATA[Surya Sathi]]></dc:creator><pubDate>Mon, 05 Jan 2026 06:30:52 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1767457138033/2f5dec08-985c-4e8c-9000-12f84190cb48.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Assume you are making a very simple login system. A user sends a request with a username and password. Let us list down what all needs to be present in the system.</p>
<ul>
<li><p>If the credentials are valid, a session should be created.</p>
</li>
<li><p>All the authorized actions by that user after login should be allowed until session expiry.</p>
</li>
<li><p>If the credentials are not valid, failed attempts counter should be incremented.</p>
</li>
<li><p>A user must not appear logged in unless they actually are.</p>
</li>
<li><p>A user must be logged in through only one device at a time.</p>
</li>
<li><p>Logout must reliably revoke access.</p>
</li>
<li><p>No user should gain access due to race conditions.</p>
</li>
<li><p>Login should be fast.</p>
</li>
<li><p>Auth checks should be cheap (they happen on every request).</p>
</li>
<li><p>System must support many concurrent users.</p>
</li>
<li><p>Requests may be retried.</p>
</li>
<li><p>State must not be corrupted by retries or races.</p>
</li>
</ul>
<h2 id="heading-what-is-a-state">What is a State?</h2>
<p>Whenever a user authenticates themselves with their credentials, and expects all the further requests to be allowed. But in order to do that system must remember that the user has logged in already, in the form of some data stored somewhere. That remembered data is called a <em>state</em>.</p>
<p><em>What</em> that remembered data is will change based on the problem statement you are dealing with.</p>
<p>The most obvious piece of state in this system, is the session itself — some representation that the user is authenticated. This can be a session ID or a token, stored in a cache or a database record. But once you start looking closely, you will find more pieces of state. This system needs to remember a lot more data - failed login attempts, expiry times, user permissions/roles for RBAC etc.</p>
<p>A useful way to recognize state is to ask a simple question: if the correctness of a request depends on something that happened earlier, you are dealing with state.</p>
<p>Once you see it this way, state appears everywhere.</p>
<h3 id="heading-local-state">Local State</h3>
<pre><code class="lang-mermaid">graph TD
    U[User] --&gt; |Login request| A[Server A]
    U --&gt; |Other request| B[Server B]

    A --&gt;|Check| M1[Session present in Memory A]
    B --&gt;|Check| M2[Session not present in Memory B]

    A --&gt;|Accepted| U
    B --&gt;|Rejected| U
</code></pre>
<p>Initially, imagine all of this running on a single server. When a user logs in, the server stores the session in memory. When the user makes another request, that same server checks its memory and validates the session. Everything works because there is only one place where truth lives.</p>
<p>Problems begin when we introduce horizontal scaling.</p>
<p>If there are multiple servers, requests can land on any of them. If session state remains stored in the memory of the server that handled the login request, other servers will not see it. This shows the major limitation of local state.</p>
<p>Local, in-memory state only works when there is exactly one server handling all requests. Once traffic can land on multiple servers, state must move somewhere shared.</p>
<h3 id="heading-shared-state">Shared State</h3>
<pre><code class="lang-mermaid">graph TD
    U[User] --&gt; |Login request| A[Server A]
    U --&gt; |Other request| B[Server B]

    subgraph Shared State
        DB[(Session Store)]
    end

    A -.-&gt; |Stored| DB
    B -.-&gt; |Retrieved| DB

    A --&gt;|Accepted| U
    B --&gt;|Accepted| U
</code></pre>
<p>So we move session state into a database or a distributed cache. Now every server can read and write the same session data. This fixes the immediate correctness issue, but it introduces a new set of tradeoffs.</p>
<p>Accessing shared state requires <strong>network calls</strong> instead of memory access. These calls are slower, can fail, and can return <strong>outdated information</strong> depending on how the storage system works. The system is now relying on an external component to provide a consistent view of state.</p>
<h3 id="heading-statelessness">Statelessness</h3>
<p>At this point, application servers are often described as stateless. What this really means is that servers do not own authoritative state in their own memory or disk. Any server can handle any request because all necessary state lives elsewhere.</p>
<p>Statelessness doesn’t mean that state doesn’t exist but that it exists somewhere else — databases, caches, or other external systems.</p>
<h2 id="heading-correctness-consistency-and-order">Correctness: Consistency and Order</h2>
<p>An important consideration that emerges is reads vs writes. Most of the requests simply check (<em>aka read</em>) the state — Is this session valid? Does this user have permissions?</p>
<p>But writes happen less frequently — session creation happens once when you login, session deletion happens once when you log out.</p>
<p>This distinction matters because reads happen more often and are also easier to scale. But when dealing writes the question of correctness appears.</p>
<p>Now assume that the state is also scaled across multiple servers to maintain availability — just like your login application.</p>
<h3 id="heading-strong-vs-eventual-consistency">Strong vs Eventual Consistency</h3>
<p>Consider a user logging in. A server validates credentials and writes a new session to the database. The database confirms the write. Immediately after, the user sends another request that lands on a different server, which reads the session from the database.</p>
<p>The system must answer a precise question: after a successful login, should other servers be able to observe that session almost immediately? Or is it okay, if the updates are slightly delayed?</p>
<p>If the database guarantees that once it acknowledges a write, all subsequent reads will observe it, then the system provides <strong>strong consistency</strong>. In this case, the database ensures that the write is fully committed before responding, and that any server reading afterward sees the same result.</p>
<p>But what mostly happens is, databases acknowledge a write but some replicas may still return stale data for a short period. The difference lies in how much coordination the database system enforces before acknowledging a write. This is called <strong>eventual consistency.</strong></p>
<p>This guarantee of strong consistency, is not free. Internally, the database must decide when a write is considered complete. If data is replicated, the database must decide whether to wait for all replicas, some replicas, or just one before acknowledging success. If one replica is slow or unreachable, the database must decide whether to wait, reject the write, or proceed anyway. <strong>These decisions affect both correctness and latency.</strong></p>
<p>And for eventual consistency, whether it is acceptable depends on the system’s requirements. For login and authentication, immediate visibility is often expected. For other types of data, short delays may be acceptable.</p>
<h3 id="heading-order">Order</h3>
<pre><code class="lang-mermaid">sequenceDiagram
    participant C1 as Client 1
    participant C2 as Client 2
    participant DB

    C1-&gt;&gt;DB: Login
    C2-&gt;&gt;DB: Logout
    DB--&gt;&gt;C1: OK
    DB--&gt;&gt;C2: OK
</code></pre>
<p>Now imagine a user clicks <em>login</em> on one device and clicks <em>logout of all devices</em> almost immediately on another device. These two operations may be handled by different servers and reach the database close together in time. The final state depends entirely on the order in which the database applies these updates.</p>
<p>If the system applies the <em>login</em> write first and the <em>logout</em> write second, the user ends up logged out. If it applies them in the opposite order, the user ends up logged in. Both requests are valid, but the outcome depends on ordering.</p>
<p>In this case, the application servers may not be in charge of this order. Maybe the database does. But whichever is in charge, it must impose a single, consistent sequence on concurrent writes to the same piece of state so that all readers observe the same result.</p>
<h3 id="heading-locking">Locking</h3>
<p>This ordering problem becomes even more visible when multiple requests attempt to modify the same data concurrently. Assume multiple failed login attempts incrementing a counter at the same time.</p>
<p>If these updates are applied without control, one update may overwrite another, leading to incorrect counters. To prevent this, <strong>locking</strong> is one of the ways that databases use apart from optimistic concurrency and version checks. Regardless of the mechanism, the goal is the same: ensure conflicting updates do not silently overwrite each other. When an update is being done to a record, any other updates to that record must wait or fail. This ensures that updates are applied one at a time and in a well-defined order.</p>
<p>Locking is not something the application servers coordinate explicitly. It is enforced by the database as part of managing shared state correctly.</p>
<h3 id="heading-transactions">Transactions</h3>
<p>Often, a login operation involves multiple related updates. A session is created, failed login counters are reset, and timestamps are updated. If one of these changes succeeds and another fails, the system ends up in an inconsistent state. To prevent this, databases provide transactions, which allow a group of changes to be applied together or roll back applied changes if any one of them fails.</p>
<h3 id="heading-failure-is-in-pov">Failure is in POV</h3>
<p>Suppose a login request reaches server, validated, session created but the response is lost due to some network issue. But the system has no way of knowing once the response leaves its area, so it regards it as request succeeded.</p>
<p>Now, if you look at it from system’s POV:</p>
<ul>
<li><p>Session created → Succeeded</p>
</li>
<li><p>Session not created → Failed</p>
</li>
</ul>
<p>If you look at it from client’s POV:</p>
<ul>
<li><p>Received success response → Succeeded</p>
</li>
<li><p>Received failed response or didn’t receive one at all → Failed</p>
</li>
</ul>
<p>But here, the request succeeded from the system’s view and failed from the client’s view at the same time.</p>
<h3 id="heading-retries">Retries</h3>
<p>Now user sends another login request:</p>
<ul>
<li><p>If the system blindly creates another session in DB, there will be duplicate sessions created if its a valid request</p>
</li>
<li><p>Failed attempts counter will be incremented if the request fails <em>while</em> there is already a valid session.</p>
</li>
</ul>
<p>To avoid this, operations must be designed so that retrying them does not cause incorrect state. This property is called <strong>idempotency</strong>.</p>
<p>An idempotent operation can be executed multiple times safely. The result is the same as if it were executed once.</p>
<p>In this use case, this means, when a user retries, the system must recognize repeated requests, should either return an existing session or invalidate the existing one and create a new session.</p>
<h2 id="heading-considering-transactions">Considering Transactions</h2>
<p>So far, transactions have helped us keep related changes consistent. But transactions have limits.</p>
<p>They work well when:</p>
<ul>
<li><p>All data lives in one database</p>
</li>
<li><p>The database can enforce locks</p>
</li>
<li><p>Failures are rare and short-lived</p>
</li>
</ul>
<p>They struggle when:</p>
<ul>
<li><p>Data spans multiple systems</p>
</li>
<li><p>Systems fail independently</p>
</li>
</ul>
<p>Imagine your login flow now touches:</p>
<ul>
<li><p>A database like Postgres</p>
</li>
<li><p>A cache like Redis</p>
</li>
<li><p>A rate-limiting service</p>
</li>
<li><p>An audit log</p>
</li>
</ul>
<p>If any one of these fails midway, a transaction that spans all of them becomes slow, fragile, or impossible.</p>
<p>So you will be forced to either accept that system can be inconsistent for a while before <strong>eventually reaching consistency</strong>, or design the system to deal with this chaos for <strong>strong consistency.</strong></p>
<h2 id="heading-performance-vs-correctness">Performance vs Correctness</h2>
<p>Every decision we have discussed so far, has a cost:</p>
<p>If you want correctness, where there is:</p>
<ul>
<li><p>immediate visibility of writes</p>
</li>
<li><p>strict ordering</p>
</li>
<li><p>transactional guarantees</p>
</li>
</ul>
<p>Then your system must:</p>
<ul>
<li><p>Wait for confirmations</p>
</li>
<li><p>Reject or delay requests</p>
</li>
</ul>
<p>This means a login request may block longer, fail more often during outages within the system, or require retries more often — all of which affect user experience.</p>
<p>If you want better performance with:</p>
<ul>
<li><p>Faster reads</p>
</li>
<li><p>Faster writes</p>
</li>
</ul>
<p>You have to accept:</p>
<ul>
<li><p>Data may be outdated sometimes</p>
</li>
<li><p>Some clients receiving new data and some receiving older data.</p>
</li>
</ul>
<p>So you are trading off consistency.</p>
<h2 id="heading-find-something-hidden-so-far">Find something hidden so far</h2>
<p>If you look at all the decisions so far — deciding upon the order of writes, checking the availability of replicas, replicating data across them, order, locking, transactions — the database has been making some decisions for correctness. This is what we call <em>coordination</em>.</p>
<p>But as systems grow and you stop depending only on a database and start including other components like caches or even other types of databases, your system’s state gets distributed — different parts of state reside in different places.</p>
<p>That means, you can’t depend on any external component for ensuring correctness of operations.</p>
<p>State is unavoidable and when you have states distributed, you must coordinate between them. This is the moment when coordination stops being implicit and becomes explicit.</p>
<p>The question now is not <em>“how do we scale?</em>” anymore but “<em>how do we handle coordination among different components in the system?”</em></p>
]]></content:encoded></item><item><title><![CDATA[Scaling & Load Balancing]]></title><description><![CDATA[If you have a single road, the 100 cars might take 5 minutes on average to cross the road. The throughput here is 20 cars/minute and latency is 5 minutes.
But if you have 5 roads, they might take only 2 minutes on average. So the throughput increased...]]></description><link>https://blog.suryasathi.com/scaling-and-load-balancing</link><guid isPermaLink="true">https://blog.suryasathi.com/scaling-and-load-balancing</guid><category><![CDATA[Load Balancing]]></category><category><![CDATA[Load Balancer]]></category><category><![CDATA[scalability]]></category><category><![CDATA[vertical scaling]]></category><category><![CDATA[horizontal scaling]]></category><category><![CDATA[scaling]]></category><dc:creator><![CDATA[Surya Sathi]]></dc:creator><pubDate>Mon, 29 Dec 2025 06:30:34 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1767455515730/78009a1a-c6c6-4651-9de8-586e936e7de1.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>If you have a single road, the 100 cars might take 5 minutes on average to cross the road. The throughput here is 20 cars/minute and latency is 5 minutes.</p>
<p>But if you have 5 roads, they might take only 2 minutes on average. So the throughput increased to 50 cars/minute and latency reduced to 2 minutes.</p>
<p>Adding roads increases both throughput <em>and</em> reduces latency because it increases capacity.</p>
</blockquote>
<p>We ended the <em>Latency vs Throughput</em> article with this example. It is a bit simplified and you will notice that soon. Anyways, now you can interpret this example as one of the two things:</p>
<ol>
<li><p>Adding more physical hardware like more CPU cores, memory etc. to a single server, so you get more and faster queues to process the requests. Or,</p>
</li>
<li><p>Adding more identical servers, so if a server is already under load, another server will take up new requests.</p>
</li>
</ol>
<p>So bottom line, scaling increases system capacity. So under load, you get reduced latency with scaling.</p>
<p>This is the core idea of scaling. Whichever scaling method you choose, you are basically trying to widen the bottleneck to reduce contention. In <strong>vertical scaling,</strong> you will be making a single server more <em>bulky</em>, to be able to handle more requests and in <strong>horizontal scaling,</strong> you will be adding more servers and <em>distribute</em> the requests between them.</p>
<h2 id="heading-what-vertical-scaling-is">What Vertical Scaling Is</h2>
<p>You will be generally improving your server in one of the following ways, core idea being reducing latency and increasing throughput:</p>
<ul>
<li><p>If your application is CPU heavy, you will be increasing the CPU cores for more parallel execution of incoming requests.</p>
</li>
<li><p>You might be increasing the RAM, if the computation of more requests needs much more memory than what’s available.</p>
</li>
<li><p>In case, it is disk I/O heavy, you will be updating to faster SSDs to reduce request processing time.</p>
</li>
</ul>
<h3 id="heading-benefits">Benefits</h3>
<ul>
<li><p>Since only one server will be communicating with your databases, you will <em>likely</em> have fewer data inconsistency issues.</p>
</li>
<li><p>And this is the only server that will be serving the requests, you can often simplify state management by keeping it local.</p>
</li>
<li><p>Using in-memory cache and disk for storage, you won’t be making the, comparatively slower, network calls to cache and databases.</p>
</li>
</ul>
<h3 id="heading-tradeoffs-and-limits">Tradeoffs and Limits</h3>
<ul>
<li><p><em>Obviously</em>, you have a single point of failure. If this system goes down, all the incoming requests will fail.</p>
</li>
<li><p>As you keep adding multiple processes and threads, you have to be careful of memory locks and database atomicity to prevent race conditions.</p>
</li>
<li><p>You can scale hardware only so much before hitting either physical limit or budget limit. As you add more and more of RAM, disk or a better CPU, your costs start increasing <em>non-linearly.</em></p>
</li>
</ul>
<p>Essentially, a bigger machine does not eliminate contention. Instead you are taking on the unsolved risk of single point of failure, need to robustly test the code to prevent race conditions and mainly every growing cost to increase physical limits to keep up with more and more requests. And beyond a point, vertical scaling hits diminishing returns due to shared resources and serial execution paths.</p>
<p>This makes <strong>horizontal scaling not a choice, but a <em>necessity</em>.</strong></p>
<h2 id="heading-what-horizontal-scaling-is">What Horizontal Scaling Is</h2>
<p>Instead of keeping on bulking up one server, you will add more and more regular servers to handle more requests. So instead of one big queue, you have many smaller queues. This drops the latency per node and increases overall throughput.</p>
<p>Now, in order to distribute the requests between these servers, you will use a load balancer.</p>
<h3 id="heading-load-balancer">Load Balancer</h3>
<p>Since you have multiple servers and each of them will have their own IP address, how will you choose which IP to map your DNS domain to?</p>
<p>Simple. Add another server in front of them and map its IP. And when requests hit this mediator server, you will route them to your servers which will be actually serving the requests, get the response and send it back. This mediator is called the <strong>load balancer.</strong> Because this will be balancing the load among the servers by routing the requests.</p>
<pre><code class="lang-mermaid">graph TD
    R1((Request 1))
    R2((Request 2))
    R3((Request 3))
    R4((Request 4))
    R5((Request 5))
    LB{Load Balancer}
    N1[Node 1]
    N2[Node 2]
    N3[Node 3]
    N4[Node 4]
    N5[Node 5]
    R1 --&gt; LB
    R2 --&gt; LB
    R3 --&gt; LB
    R4 --&gt; LB
    R5 --&gt; LB
    LB --&gt; N1
    LB --&gt; N2
    LB --&gt; N3
    LB --&gt; N4
    LB --&gt; N5
</code></pre>
<p>But how will you route the requests?</p>
<p>Will you choose randomly one of the servers? Or maybe you can send first request to <em>server 1</em>, next one to <em>server 2</em>, and the next one to <em>server 3</em> etc. This strategy is called <strong>Round Robin.</strong> Or maybe keep track of which server is serving how many number of requests and route new requests to the one with the least load. This strategy is called <strong>Least Connections</strong>. These different ways give you different ways of balancing the load.</p>
<p>Apart from this, this load balancer will also keep track of health status of each server, typically by calling a health API endpoint you will be exposing, to route the requests to only the healthy ones.</p>
<h3 id="heading-benefits-1">Benefits</h3>
<ul>
<li><p>If one of your server fails, your load balancer will just route it to another server. Essentially, giving your service no down time.</p>
</li>
<li><p>Theoretically, you don’t have the same physical limits as vertical scaling. You can increase as many nodes as needed to handle more and more requests.</p>
</li>
<li><p>Beyond a certain scale, cost wise, horizontal scaling becomes more viable than vertical scaling.</p>
</li>
</ul>
<h2 id="heading-new-problems-with-horizontal-scaling">New Problems with Horizontal Scaling</h2>
<p>Horizontal scaling means multiple machines working together. The moment you do that, you enter the world of distributed systems, where new classes of problems appear.</p>
<h3 id="heading-io-network">I/O → Network</h3>
<p>In a single machine, different services communicate with function calls. They share memory and storage. But once you scale, you often introduce a central cache and memory in order to maintain consistency.</p>
<p>Now they are no longer, internal calls to RAM or disk but network calls to cache and database servers, increasing latency from microseconds to milliseconds. Add to that there will be retries, exponential backoffs in case of failed network calls to those services.</p>
<p><em>Now the latency won’t be just because of computation but because of communication as well.</em></p>
<h3 id="heading-partial-failures">Partial Failures</h3>
<p>When you have multiple nodes, one of them can be slow, one might be down, one might be good, one might already be overloaded. You can’t assume that all requests will succeed and need to account for retries, timeouts.</p>
<p>And when multiple services depend on each other, you must also account for cascading failures and be careful not to have a single point of failure.</p>
<h3 id="heading-inconsistency-in-data">Inconsistency in data</h3>
<p>Now not just you but databases also implement horizontal scaling. Now look at these scenarios keeping that in mind.</p>
<p>Since data is outside of a server’s responsibility, updates can take time to propagate and replicate across machines of database, resulting in one person seeing updated data and one seeing outdated data.</p>
<p>If some of your nodes are slower, updates to data might go out of order.</p>
<p>If a person updates and another requests data, update might end up happening later than returning data resulting in outdated views.</p>
<p>So, in practice, many systems relax strong consistency to improve availability and scalability. But some systems might prefer strong consistency rather than availability — for example, banks. The choice depends on your use case and industry.</p>
<h3 id="heading-load-balancing-tradeoffs">Load Balancing Tradeoffs</h3>
<p>Load balancing doesn’t split traffic evenly.</p>
<p><strong>Round Robin</strong> strategy, by default assumes that all servers are equally fast, and spreads them evenly only by count, so each server gets equal number of requests.</p>
<p>But counting requests is not the same as measuring load. Slow nodes become bottlenecks. If the event loop is filled on a slow node, it will keep the remaining requests in queue until the event loop gets free. This is called <strong>head-of-line blocking.</strong> In this case, latency explodes for further requests on that node.</p>
<p><strong>Least Connections</strong> strategy is slightly better but even it doesn’t know about how much resource consumption that request leads to.</p>
<p>For example, a <code>/health</code> and a <code>/orders/123</code> requests don’t really give any context to the load balancer, but a <code>/health</code> is not a heavy call and gives an immediate response but <code>/orders/123</code> can result in a DB query along with auth checks on DB side which might be slower and more resource intensive but at the same time a <code>/orders/456</code> might actually give the result from a DB cache itself resulting in a faster response. So if <em>node A</em> gets 2 cheap requests and <em>node B</em> gets 1 expensive requests, <em>node B</em> is not loaded enough in the eyes of the load balancer as it has only one active connection. So it will route the next connection to node B, which might turn out to be an expensive one.</p>
<p>A load balancer doesn’t know about any downstream services as well. In the same example, if the DB is not reachable, the node will keep on trying until it hits retry limit. Add that with an exponential backoff, resulting in a connection living longer on that node. But load balancer will keep sending it more requests, which will be just waiting in a queue until the previous requests are freed from the event loop, adding latency.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Now what would one want to use in a real system?</p>
<p>Since nobody likes a single point of failure, you would go with horizontal scaling generally but you don’t take very minimal nodes and scale them recklessly. Instead you find a sweet spot, <em>a hybrid</em>, between horizontal and vertical scaling to decide upon the hardware specs of each node in a node group based on your budget and latency requirements.</p>
<h2 id="heading-state-a-subtle-shift-horizontal-scaling-forces">State - A Subtle Shift Horizontal Scaling Forces</h2>
<p>Horizontal scaling looks simple as long as each server can handle requests independently — an assumption that rarely holds in real systems.</p>
<p>Once traffic can land on any node, state — things like sessions, counters, or cached data — that once lived comfortably in a single machine — in memory or on disk — can no longer be relied on. A request handled by one server may need information that was created or updated by another.</p>
<p>At this point new questions arise. Do we maintain the same state in all the machines somehow? Or do we completely avoid state? Or should we store the state somewhere externally and retrieve it when needed? But then how confident can we be about consistency?</p>
<p>This is where horizontal scaling in systems stops being just about adding servers and becomes a question of how state is managed across them.</p>
<p>I will explore these questions in my next article.</p>
]]></content:encoded></item><item><title><![CDATA[Latency vs Throughput]]></title><description><![CDATA[Background
Assume your browser made a request for a webpage. It flows like this:
flowchart TD
    subgraph Client
        CApp[Application]
        COS[OS]
        CNIC[Network Hardware]
        CApp --> COS --> CNIC
        CNIC --> COS --> CApp
   ...]]></description><link>https://blog.suryasathi.com/latency-vs-throughput</link><guid isPermaLink="true">https://blog.suryasathi.com/latency-vs-throughput</guid><category><![CDATA[latency]]></category><category><![CDATA[throughput]]></category><category><![CDATA[concurrency]]></category><dc:creator><![CDATA[Surya Sathi]]></dc:creator><pubDate>Tue, 23 Dec 2025 07:07:51 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1766916666018/c928dc6f-e0a6-4b35-887c-da3b9b0b0d62.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-background">Background</h2>
<p>Assume your browser made a request for a webpage. It flows like this:</p>
<pre><code class="lang-mermaid">flowchart TD
    subgraph Client
        CApp[Application]
        COS[OS]
        CNIC[Network Hardware]
        CApp --&gt; COS --&gt; CNIC
        CNIC --&gt; COS --&gt; CApp
    end

    subgraph Network
        WR[Wi-Fi Router]
        ISP[ISP Link]
        IR[Internet Routers]
        CNIC --&gt; WR --&gt; ISP --&gt; IR
        IR --&gt; ISP --&gt; WR --&gt; CNIC
    end

    subgraph Server
        SNIC[Network Hardware]
        SOS[OS]
        SApp[Application]
        IR --&gt; SNIC --&gt; SOS --&gt; SApp
        SApp --&gt; SOS --&gt; SNIC --&gt; IR
    end
</code></pre>
<p>When an application wants to communicate using TCP, it asks the OS to create a socket. The OS creates a socket data structure that holds state and buffers. When the application writes data, the data goes into the socket’s send buffer. The OS networking stack breaks this data into packets and places them into kernel network buffers.</p>
<p>The network hardware (for example, a Wi-Fi card) pulls packets from these buffers and transmits their bits sequentially over the physical link (Wi-Fi radio channel). Because the link can only transmit one stream of bits at a time, packets from different applications and sockets are serialized and wait in queues.</p>
<p>Each packet is received by the Wi-Fi router, queued, and forwarded over the ISP’s fiber optic link. The packets pass through multiple routers on the internet, where they may again be queued and delayed, until they reach the server.</p>
<p>As packets arrive, the server’s OS acknowledges them and delivers the data to the server application. Acknowledgments travel back along the reverse path <em>(may not be same intermediate routers)</em>. While acknowledgments are returning, the client continues sending more packets.</p>
<p>The server begins processing the request before all data arrives. When it generates a response, that response is broken into packets again and sent back through the same sequence of links, queues, and buffers in reverse, until the client receives it.</p>
<p>Meanwhile just like you, many different people make these requests, so all these requests are accommodated by the server by spreading them over through event loops, threads and processes <em>(to understand these topics, please check previous articles)</em>.</p>
<h3 id="heading-bottlenecks">Bottlenecks</h3>
<p>Now if you see the above flow, there are several bottlenecks:</p>
<ol>
<li><p>The link from your system’s Wi-Fi card to your Wi-Fi router.</p>
</li>
<li><p>Your Wi-Fi router to your ISP fiber optic cable</p>
</li>
<li><p>Internet to the server’s network hardware</p>
</li>
<li><p>Inside the server, how it manages multiple requests</p>
</li>
</ol>
<h3 id="heading-definitions">Definitions</h3>
<p><strong>Latency</strong> is the end-to-end time between sending a request and receiving the response, including network delay, queueing, and server processing.</p>
<p><strong>Network Throughput</strong> is the amount of data that can be sent over a unit of time. In our case, to measure a server efficiency, you can think of it as, amount of requests that can be handled by the server in a unit of time, call it <strong>Server Throughput</strong>.</p>
<h2 id="heading-how-latency-occurs">How latency occurs</h2>
<p>From a client POV, the latency is mostly because of the hardware limits. Even if your OS and applications can handle thousands of requests simultaneously, at the end they have to be sent through your system’s network layer that can send only a finite amount of data at a time. So after your OS packages the data, your network layer will serialize the packets and sends them one by one. As a result, when there is more data, it will be automatically queued in buffers, which means there is a waiting time, meaning there is a latency.</p>
<p>Now, even if you are sending just one unit of data, it still needs to travel over networks to reach server, then server needs to compute and send a response which again goes through networks to reach your system. which will always take <em>some</em> amount of time, at least in milliseconds. So <strong>latency will always be there.</strong></p>
<p>So we don’t try to remove latency, <em>it is impossible</em>, instead we try to reduce latency.</p>
<h3 id="heading-what-about-throughput">What about throughput</h3>
<p>Now if you want your request data to be sent over as quickly as possible, logically you want your buffer queue to be empty so that the request is handled <em>immediately</em>. But if your queue is always handling as less data as possible, it means you are sending very low amount of data per second, which means very low throughput. But since you don’t want your hardware not be used to its full efficiency, you want your queue to be filled, meaning some requests needs to wait in the queue before being sent, meaning higher latency. So you notice the pattern:</p>
<pre><code class="lang-mermaid">flowchart LR
    subgraph Client[Single Client on Network]
        direction LR
        LT[Low Network Throughput]
        EQ[Empty Queue]
        NW[No Wait Time]
        LL[Low Latency]
        LT --&gt; EQ --&gt; NW --&gt; LL
    end
</code></pre>
<pre><code class="lang-mermaid">flowchart LR
    subgraph Client[Single Client on Network]
        direction LR
        HT[High Network Throughput]
        FQ[Filled Queue]
        WT[Wait Time]
        HL[High Latency]
        HT --&gt; FQ --&gt; WT --&gt; HL
    end
</code></pre>
<blockquote>
<p>Think of a network as a road. Number of cars that can pass through per a unit of time is called the network throughput. How long a car takes to go from start to the end is called the latency.</p>
</blockquote>
<p>Now it <em>might</em> seem like latency and throughput are proportional, but actually they aren’t. The difference comes from the perspective. You see, from your system’s perspective this is the case when it has a network to itself. But look at it from a shared network’s perspective.</p>
<p>If your Wi-Fi is shared by 5 devices, all 5 devices can’t send data to Wi-Fi at the same time, since all of you share the same physical medium even though you have 5 different logical links. So while one device sends data, the others have to wait for their turn. The same happens when you are downloading as well, while your device is receiving its packets, the other device needs to wait to receive its packets.</p>
<p>So ultimately, when you have more and more devices sharing a network, the throughput decreases <em>per device</em>, and the latency will increase. This is the reason why if someone on you Wi-Fi network downloads a movie, your YouTube video’s quality drops.</p>
<p>Similarly, no matter how much of data is flowing between your Wi-Fi router and your systems, the throughput of the ISP’s fiber optic cable to your router will again limit how much data can flow.</p>
<p>Add to that, as utilization approaches the capacity of a link or server, queueing delay increases non-linearly, which is why latency can suddenly spike even when throughput only increases slightly.</p>
<pre><code class="lang-mermaid">flowchart TD
    subgraph Network[Shared Network]
        direction LR
        MD[More Devices]
        LT[Low Throughput per device]
        WT[Higher Wait Time]
        HL[High Latency]
        MD --&gt; LT --&gt; WT --&gt; HL
    end

    subgraph ISP
        direction LR
        R[Router]
        FO[Fiber Optic Cable]
        PCT[Pre-configured Throughput]
        IWT[Wait time before entering Internet]
        L[Latency]
        R --&gt; FO --&gt; PCT --&gt; IWT --&gt; L
    end

    Network --&gt; ISP
</code></pre>
<h2 id="heading-role-of-a-server-in-latency-and-throughput">Role of a server in latency and throughput</h2>
<p>Now if a server receives just a single request, it will process it immediately and send a response. But if it gets multiple requests at a time, based on how the code was written, it will distribute among its event loops, threads and processes to try to handle them concurrently, <em>and if possible</em>, parallelly as well. This concurrency make it seem like they are being handled simultaneously and moderately reducing the response time of a request, thereby directly affecting the latency.</p>
<p>But if it gets overwhelmed by requests and its hardware limits of threading are reached, the requests will be queued and thereby wait time increases, increasing latency. Now here, number of requests it can handle per a unit of time is called the throughput.</p>
<pre><code class="lang-mermaid">flowchart LR
    subgraph Server
        direction LR
        CP[Efficient Concurrency and Parallelism]
        HR[Higher number of requests]
        HT[High Throughput]
        LRT[Lower Response Time]
        LL[Low Latency]
        CP --&gt; HR --&gt; HT --&gt; LRT --&gt; LL
    end
</code></pre>
<h2 id="heading-how-to-maximize-throughput-and-reduce-latency">How to maximize throughput and reduce latency</h2>
<blockquote>
<p>Similar to the previous traffic example, say you have a 100 cars.</p>
<p>If you have a single road, the 100 cars might take 5 minutes on average to cross the road. The throughput here is 20 cars/minute and latency is 5 minutes.</p>
<p>But if you have 5 roads, they might take only 2 minutes on average. So the throughput increased to 50 cars/minute and latency reduced to 2 minutes.</p>
<p>Adding roads increases both throughput <em>and</em> reduces latency because it increases capacity.</p>
</blockquote>
<p>You can increase the throughput of a single server by increasing its hardware limits, which will let it handle more requests simultaneously, reducing latency. But there’s only so much you can do that way.</p>
<p>Here’s where the vertical scaling vs horizontal scaling comes into the picture, which I will cover in the next article.</p>
]]></content:encoded></item><item><title><![CDATA[From Your System to the Cloud]]></title><description><![CDATA[Imagine you’ve built a brilliant new web application. It runs flawlessly on your laptop. But now comes the harder question: how do you make it available to the world?
This is the central problem of application deployment, one that has evolved dramati...]]></description><link>https://blog.suryasathi.com/from-your-system-to-the-cloud</link><guid isPermaLink="true">https://blog.suryasathi.com/from-your-system-to-the-cloud</guid><category><![CDATA[containerization]]></category><category><![CDATA[Docker]]></category><category><![CDATA[Kubernetes]]></category><category><![CDATA[Cloud]]></category><category><![CDATA[Cloud Computing]]></category><dc:creator><![CDATA[Surya Sathi]]></dc:creator><pubDate>Wed, 27 Aug 2025 06:30:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1765779430726/5304cad1-d342-4c76-9716-65950e52bc8d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Imagine you’ve built a brilliant new web application. It runs flawlessly on your laptop. But now comes the harder question: how do you make it available to the world?</p>
<p>This is the central problem of application deployment, one that has evolved dramatically over the last two decades. In the past, teams bought and managed physical servers — a slow, capital-heavy, and inflexible process. Today, cloud providers like AWS, GCP, and Azure offer abstraction layers that let you focus less on hardware and more on your code.</p>
<p>Broadly, there are three primary ways to package and run your applications in the cloud: Virtual Machines, Containers, and Serverless Runtimes. Each represents a step in the journey toward higher abstraction and efficiency.</p>
<h2 id="heading-virtual-machines-the-starting-point"><strong>Virtual Machines: The Starting Point</strong></h2>
<p>A VM is essentially a full emulation of a physical computer — complete with its own operating system (OS), CPU, memory, and storage. A single physical host can run many VMs, each isolated from the others.</p>
<p>Before the rise of microservices and rapid deployment, Virtual Machines (VMs) were the default way to run applications in the cloud.</p>
<p>Cloud providers still offer VMs as their most fundamental compute service:</p>
<ul>
<li><p><strong>AWS</strong> offers Amazon EC2 (Elastic Compute Cloud), where you launch instances based on Amazon Machine Images (AMIs).</p>
</li>
<li><p><strong>GCP</strong> offers Google Compute Engine (GCE).</p>
</li>
<li><p><strong>Azure</strong> offers Azure Virtual Machines.</p>
</li>
</ul>
<p>Instance types (e.g., AWS’s t3.micro vs. c5.4xlarge) let you tune CPU, memory, and networking for your workload. You get <strong>full control</strong> over the OS and software stack — great for workloads that need custom environments.</p>
<p><strong>Tradeoff:</strong> That control comes at a cost. VMs are heavy, slow to boot, and resource-inefficient compared to newer options. If you need fast scale-up or scale-down, VMs often lag behind.</p>
<h2 id="heading-containers-no-more-it-works-on-my-machine-mostly"><strong>Containers: No more "it works on my machine" (mostly)</strong></h2>
<p>The limitations of VMs drove adoption of <strong>containers</strong>.</p>
<p>A container packages an app and all its dependencies into an isolated unit. Unlike VMs, containers <strong>share the host OS kernel</strong>, which makes them dramatically lighter and faster. You can spin up thousands of containers in seconds.</p>
<p>Instead of having a full OS for each instance, containers run on a shared kernel, making them incredibly lightweight, portable, and fast to start.</p>
<p>Think of it like this: a VM is a house with its own foundation, walls, and plumbing, while a container is an apartment within a building. The apartment shares the building's foundation and common utilities but is completely separate from other apartments. This shared-resource model allows for much more efficient resource utilization.</p>
<p>This efficiency underpins <strong>microservices architectures</strong> and modern DevOps practices.</p>
<p>A key player in this container revolution is <strong>Docker</strong>.</p>
<h3 id="heading-docker-standardizing-containers"><strong>Docker: Standardizing Containers</strong></h3>
<p>Docker is an open-source platform that standardizes how applications are packaged and distributed using containers. It lets you define the environment (libraries, dependencies, config) alongside your application code using a <strong>Dockerfile</strong>. From this file, Docker builds an <strong>image -</strong> a blueprint containing everything needed to run the app.</p>
<p>When this image runs, it becomes a <strong>container</strong>.</p>
<p>Because the container includes the full runtime context, it behaves consistently across environments — solving the classic <em>“it works on my machine”</em> problem (for the most part) because the containerized application will behave identically in any environment where Docker is installed, from a developer's laptop to a cloud server.</p>
<p>Docker builds images in layers, allowing shared caching between images. This reduces build times and storage — a key performance optimization. Advanced Docker topics like volume mounts for persisting data outside containers, multi-stage builds to reduce image size by separating build and runtime dependencies, and caching strategies offer further optimization, depending on your deployment needs.</p>
<p>While Docker makes it easy to create and run a single container, what happens when you need to run hundreds or even thousands of them? This is where container orchestration comes in, and the industry standard for this is <strong>Kubernetes</strong> (often abbreviated as K8s).</p>
<h3 id="heading-kubernetes-container-orchestrator"><strong>Kubernetes: Container Orchestrator</strong></h3>
<p>Kubernetes is an open-source system that automates the deployment, scaling, and management of containerized applications. It acts as a conductor for your containers, ensuring that your application is always running smoothly.</p>
<p>Imagine you're managing a fleet of delivery trucks (your containers). Docker provides you with the standardized trucks and the ability to load them. However, you'd need a dispatcher to manage the entire fleet: to decide which trucks go where, to replace a broken truck with a new one, to add more trucks during a busy season, and to ensure all trucks are working.</p>
<p>Kubernetes is that dispatcher. It groups containers into logical units called Pods, schedules them to run on available machines (Nodes), performs automatic rollouts and rollbacks for updates, and provides a way for containers to communicate with each other. This level of automation is what enables the massive scale and resilience of modern cloud-native applications.</p>
<p>But K8s adds operational complexity and it has a bit steeper learning curve. And cloud providers try to reduce that pain by providing managed services.</p>
<h3 id="heading-containerization-in-cloud"><strong>Containerization in Cloud</strong></h3>
<ul>
<li><p>AWS offers several container services, most notably Amazon EKS (Elastic Kubernetes Service) and Amazon ECS (Elastic Container Service). ECS is AWS's own container orchestration service, which is simpler and deeply integrated with the AWS ecosystem.</p>
</li>
<li><p>GCP also offers GKE (Google Kubernetes Engine) for using Kubernetes and Cloud Run for deploying individual containers.</p>
</li>
<li><p>Azure offers AKS (Azure Kubernetes Service).</p>
</li>
</ul>
<h2 id="heading-serverless-somebody-elses-server"><strong>Serverless: Somebody else's server</strong></h2>
<p>For certain types of applications—specifically, small, event-driven functions—even containers can be overkill. This is where runtimes come into play through a concept known as <strong>Serverless Computing</strong>.</p>
<p>A runtime is a language-specific environment (e.g., Node.js, Python, Java) that executes your code. In a serverless model, you simply upload your code, and the cloud provider handles everything else: provisioning a machine, providing the runtime environment, and scaling the function based on demand. You don't have to think about servers, VMs, or even containers.</p>
<ul>
<li><p>AWS offers <strong>AWS Lambda</strong>.</p>
</li>
<li><p>GCP offers <strong>Google Cloud Run Function</strong>s</p>
</li>
<li><p>Similarly, Azure offers Azure Functions.</p>
</li>
</ul>
<p>Serverless functions are triggered by events, such as an HTTP request, a file upload to an S3 bucket, or a message in a queue. You only pay for the compute time your code uses, down to the millisecond.</p>
<p>Runtimes are ideal for stateless, short-lived, event-driven tasks like processing data from a database, resizing an image after a user uploads it, or handling API requests for a simple backend.</p>
<p>But serverless isn't a free lunch. Cold starts (delay when a function is invoked after being idle), limited execution time, and observability challenges can complicate performance tuning and debugging.</p>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>The journey from a local application to a global-scale service is now relatively simple, thanks to cloud providers like AWS, Azure and GCP.</p>
<p>The problem of getting your code to run reliably and at scale has been solved through different levels of abstraction. While Virtual Machines offer the most control and isolation, Containers provide a more agile and efficient alternative, and Runtimes represent the peaks of abstraction and cost efficiency for event-driven tasks.</p>
<p>The choice depends on your specific needs, but a modern developer now has great tools to deploy their applications, no longer worrying about the physical hardware but instead focusing on what they do best: writing great applications.</p>
<h2 id="heading-food-for-thought"><strong>Food for Thought</strong></h2>
<p>While the cloud offers scalability and agility, it's not the ideal solution for every problem. The convenience of "pay-as-you-go" can lead to unpredictable costs, and the most cutting-edge technologies aren't always the most efficient for your specific use case.</p>
<p>Some organizations with predictable, high-volume workloads have found that the long-term cost of public cloud services exceeds the cost of managing their own data centers. This has led to a trend known as <strong>cloud repatriation</strong>.</p>
<p>Similarly, even within cloud, there are tradeoffs. For example, adopting a serverless architecture, while good for some applications, can become expensive and complex for certain types of workloads.</p>
<p>In 2016, Dropbox moved a significant portion of its data storage from AWS back to its own on-premises infrastructure to gain more control and reduce costs. You can read more about their decision and the technical details of their move here:</p>
<p>The Amazon Prime Video team detailed their decision to re-architect from a serverless, microservices-based system to a monolithic application, which resulted in a 90% cost reduction. You can find the summary of their case study here:</p>
<p>So, how do you decide?</p>
<ul>
<li><p><strong>Use VMs</strong> when you need full OS control, custom environments, or legacy workloads.</p>
</li>
<li><p><strong>Use Containers</strong> for microservices, DevOps pipelines, and scalable web apps.</p>
</li>
<li><p><strong>Use Kubernetes</strong> when you need container orchestration at scale <em>and</em> can afford the operational complexity.</p>
</li>
<li><p><strong>Use Serverless Runtimes</strong> for event-driven, stateless, bursty workloads where cost and simplicity matter more than control.</p>
</li>
</ul>
<p>Each layer of abstraction trades <strong>control</strong> for <strong>convenience</strong>.</p>
<p>Modern developers don’t have to worry about racking servers in a datacenter anymore but they do need to think critically about which cloud model actually fits their application. Because convenience at the wrong scale can be more expensive than running your own servers.</p>
]]></content:encoded></item><item><title><![CDATA[When Can You Break Software Design Principles?]]></title><description><![CDATA[You must have heard of SOLID, DRY and KISS design principles somewhere at some point. If you don't know what they mean let's go through now. But firstly, they are design principles and not rules - meaning, you are not strictly required to follow them...]]></description><link>https://blog.suryasathi.com/when-can-you-break-software-design-principles</link><guid isPermaLink="true">https://blog.suryasathi.com/when-can-you-break-software-design-principles</guid><category><![CDATA[Breaking-rules]]></category><category><![CDATA[SOLID principles]]></category><category><![CDATA[KISS Principle]]></category><category><![CDATA[DRY Principle (Don't Repeat Yourself)]]></category><dc:creator><![CDATA[Surya Sathi]]></dc:creator><pubDate>Wed, 20 Aug 2025 06:30:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1765779187122/21210528-3f10-4b89-834f-dd14f8598105.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>You must have heard of SOLID, DRY and KISS design principles somewhere at some point. If you don't know what they mean let's go through now. But firstly, they are design principles and not rules - meaning, you are not strictly required to follow them, but in general, it is considered that they make the program more readable and maintainable code that can be understood easily, without repetition and also be open to extensions without severe breakage.</p>
<h2 id="heading-uncle-bobs-solid-principles"><strong>Uncle Bob's SOLID Principles</strong></h2>
<p>Imagine you are building with LEGOs. The SOLID design principles are like good practices for your LEGO pieces and how you connect them, so your final creation is strong, easy to modify and doesn't collapse if you change one block.</p>
<h3 id="heading-s-single-responsibility-principle-srp"><strong>S: Single Responsibility Principle (SRP)</strong></h3>
<p>A class should have only one reason to change.</p>
<p>Let’s say you have a User class. Its job should be to manage user-related data—like name, email, etc. It should not be responsible for sending emails.</p>
<p>Why? Because:</p>
<ul>
<li><p>Sending emails is unrelated to the core idea of what a user is.</p>
</li>
<li><p>If the way you send emails changes, your User class would have to change too—introducing unnecessary fragility.</p>
</li>
</ul>
<p>Instead, you’d have a User class and a separate <code>EmailSender</code> class. Each has one job. Now, if the email logic changes, only <code>EmailSender</code> needs updating—User stays untouched. That makes your code easier to understand, test, and maintain.</p>
<h3 id="heading-o-openclosed-principle-ocp"><strong>O: Open/Closed Principle (OCP)</strong></h3>
<p>Software entities should be open for extension, but closed for modification.</p>
<p>Imagine you’re building a house. You want to add a new room later without tearing down the whole structure, right?</p>
<p>OCP means: You should be able to add new behavior to code without changing existing working code.</p>
<p>For example, suppose you’re calculating the area of shapes. If you have an <code>AreaCalculator</code> with <code>calculateCircleArea()</code>, <code>calculateSquareArea()</code> like that, you will be needed to change this to add a <code>calculateTriangleArea()</code> method later. Instead you can have a Shape interface with the method area(). You create Circle, Square, and Triangle classes that each implement Shape.</p>
<p>An <code>AreaCalculator</code> can now use <code>shape.area()</code> without caring about which shape it is. If you add a Hexagon, you don’t modify the calculator—just implement a new Hexagon class.</p>
<p>The core logic stays stable. New functionality gets added on top, not inside old code. This reduces bugs and makes extension easy.</p>
<h3 id="heading-l-liskov-substitution-principle-lsp"><strong>L: Liskov Substitution Principle (LSP)</strong></h3>
<p>Subclasses should be replaceable for their base classes without breaking the app.</p>
<p>Suppose you have a Vehicle class, and a Car extends it. Any code expecting a Vehicle should be able to work with a Car without issues.</p>
<p>This isn’t just about types—it's about behavioral compatibility. A subclass should honor the same “contract” as the parent class. A child class's implementation should not be stricter or restricting.</p>
<p>Bad LSP: If Bird has fly(), and Penguin extends Bird, but <code>Penguin.fly()</code> throws an error—your inheritance model is broken. Penguin isn't substitutable for Bird.</p>
<p>Use inheritance only when it makes logical sense. If the behaviors differ significantly, consider composition over inheritance. If a subclass violates expectations of the base class, the base class might be modeling the wrong abstraction.</p>
<blockquote>
<p>“At its heart, LSP is about interfaces and contracts, and when to extend a class versus using another strategy such as composition to achieve your goal.” – A Stack Overflow answer</p>
</blockquote>
<h3 id="heading-i-interface-segregation-principle-isp"><strong>I: Interface Segregation Principle (ISP)</strong></h3>
<p>Don’t force classes to implement methods they don’t use.</p>
<p>Think of a TV remote with 30 buttons: HDMI, lights, fans, Blu-ray controls, volume, channels. But if you only ever use volume and channels, the rest is just clutter.</p>
<p>ISP says: Split large interfaces into smaller, specific ones.</p>
<p>If you have a <code>TaskWorker</code> interface with:</p>
<ul>
<li><p><code>startTimer()</code></p>
</li>
<li><p><code>reportProgress()</code></p>
</li>
<li><p><code>sendEmailNotification()</code></p>
</li>
<li><p><code>printDocument()</code></p>
</li>
</ul>
<p>…but a class only needs the first two, it shouldn’t be forced to implement email and printing methods too. Instead, split into:</p>
<ul>
<li><p><code>WorkerInterface</code></p>
</li>
<li><p><code>EmailNotifierInterface</code></p>
</li>
<li><p><code>PrintableInterface</code></p>
</li>
</ul>
<p>This improves flexibility, reduces unnecessary dependencies, and keeps things clean and understandable.</p>
<h3 id="heading-d-dependency-inversion-principle-dip"><strong>D: Dependency Inversion Principle (DIP)</strong></h3>
<p>High-level modules shouldn’t depend on low-level modules. Both should depend on abstractions.</p>
<p>Abstractions shouldn’t depend on details. Details should depend on abstractions.</p>
<p>Suppose you have an <code>OrderProcessor</code> that:</p>
<ul>
<li><p>Uses a <code>PayPalPaymentGateway</code> to process payments.</p>
</li>
<li><p>Uses a <code>PostgreSQLOrderSaver</code> to save the order.</p>
</li>
</ul>
<p>If you later switch to GooglePay and MongoDB, you’d have to modify <code>OrderProcessor</code> directly. That’s tight coupling.</p>
<p>Instead:</p>
<ul>
<li><p>Create an abstract <code>PaymentGateway</code> interface with <code>process_payment()</code>.</p>
</li>
<li><p>Create an abstract <code>OrderSaver</code> with <code>save_order()</code>.</p>
</li>
</ul>
<p><code>OrderProcessor</code> should depend only on these abstractions.</p>
<p>Then you do:</p>
<ul>
<li><p><code>PayPalPaymentGateway</code>, <code>GooglePayGateway</code> → both implement <code>PaymentGateway</code></p>
</li>
<li><p><code>PostgreSQLOrderSaver</code>, <code>MongoOrderSaver</code> → both implement <code>OrderSaver</code></p>
</li>
</ul>
<p>Now, you can swap implementations easily—without touching business logic. This promotes loose coupling, testability, and flexibility.</p>
<h2 id="heading-dry-dont-repeat-yourself"><strong>DRY: Don't Repeat Yourself</strong></h2>
<p>This principle is probably the easiest to grasp and incredibly important.</p>
<p>It's pretty self-explanatory: every piece of knowledge must have a single, unambiguous and authoritative representation within a system.</p>
<p>Think about writing an essay. You don't write the exact same paragraph over and over again in different sections. Instead, you'd write that paragraph once and then refer to it later.</p>
<p>In programming, this means if you have the same piece of code (like a calculation, a validation rule, or a way to format data) appearing in multiple places, you should extract it into a function, a method, or a class, and then call that single piece of code wherever you need it.</p>
<p>But sometimes excessive usage of DRY can reduce readability. Also sometimes you may find yourself extracting similar looking code into same utility function, though they should be independent. Do keep that in mind.</p>
<h2 id="heading-kiss-keep-it-simple-stupid"><strong>KISS: Keep It Simple, Stupid</strong></h2>
<p>This is a classic and very practical advice.</p>
<p>The idea is that simplicity should be the key goal in design and unnecessary complexity should always be avoided.</p>
<p>When you're designing or writing code, avoid the urge to over-engineer. It can also be applied to the naming conventions, logics of functions, comments, almost everywhere. It can also be stated as - when even someone stupid looks at your program, they should still be able to understand what it's doing.</p>
<p>This also means that, don't spend a lot of time making something super-efficient if it's not a bottleneck right now. Don't use a complex design pattern if a simple function call will suffice. And if you find a part of your code becoming overly complex, break it down into smaller, simpler pieces.</p>
<p>KISS says that simple code is easier to understand, easier to debug and less prone to error. And when you yourself or someone else has to work with your code later, it makes both your lives easier.</p>
<h2 id="heading-yagni-you-arent-gonna-need-it"><strong>YAGNI: You Aren't Gonna Need It</strong></h2>
<p>The name says it all. Do not add functionality until it's actually required.</p>
<p>Think of it like packing for a trip. You might be tempted to pack all sorts of "just in case" items. Before you know it, you have a massive, heavy suitcase, and you probably won't use half the stuff in it.</p>
<p>It's the same in software development. It's about resisting the temptation to add code or features that you think you might need in the future, but aren't explicitly required right now for the current problem you're solving.</p>
<p>Adding extra code will waste time and effort, it is harder to understand later, bloats the code base and increases the risk of bugs just because you made assumptions about how the software will evolve without concrete requirements. So always follow YAGNI unless you know with high certainty that extra work will be helpful in the near future.</p>
<h2 id="heading-how-to-know-when-to-violate-a-principle">How to know when to violate a principle</h2>
<blockquote>
<p>"Pragmatism over Purity"</p>
</blockquote>
<p>Before you ever deviate, ask yourself:</p>
<ol>
<li><p>What problem does violating this principle solve right now? (e.g., "It saves us 2 days of development time for a critical deadline.")</p>
</li>
<li><p>What are the long-term consequences of this violation? (e.g., "The code will be harder to change in this one spot, but we'll likely rewrite this module anyway in 2 or 3 months.")</p>
</li>
<li><p>Are these consequences acceptable given the current situation? (e.g., "Yes, because missing this deadline means losing the client.")</p>
</li>
</ol>
<p>If you can't articulate clear, justifiable answers to these questions, then you probably shouldn't violate the principle.</p>
<h3 id="heading-when-can-you-violate-solid"><strong>When can you violate SOLID?</strong></h3>
<ul>
<li><p>When responsibilities are tightly coupled and always change together in smaller applications, you may break SRP.</p>
</li>
<li><p>For extending very small and stable modules, the overhead of introducing complex abstractions for trivial changes often outweighs the benefit of changing the module. If the code is already simple, and the change is small and unlikely to break anything else, you may break OCP.</p>
</li>
<li><p>Also when there is a fundamental change in a core component, it is not an extension but re-architecture. You will need to break OCP.</p>
</li>
<li><p>When you implement interfaces from third party libraries, you may need to implement methods you don't use, potentially breaking ISP.</p>
</li>
<li><p>You may also need to calculate the risk of breaking either YAGNI or OCP or find a balance in the initial stages of development as YAGNI says don't build for future while OCP says keep the future in mind when building.</p>
</li>
<li><p>You may also break all of SOLID principles during rapid prototyping with throwaway code.</p>
</li>
</ul>
<h3 id="heading-when-can-you-violate-dry"><strong>When can you violate DRY?</strong></h3>
<ul>
<li><p>Sometimes, two pieces of code look identical now but are logically independent and will likely evolve differently. So you write it twice, and if the third time the logic is identical, then you abstract.</p>
</li>
<li><p>If extracting a very small, simple piece of logic makes the code harder to follow and it is clearly understood inline, then repeating it might be more readable.</p>
</li>
</ul>
<h3 id="heading-when-can-you-violate-kiss"><strong>When can you violate KISS?</strong></h3>
<ul>
<li>Only when it is performance critical. Otherwise, always try to follow it.</li>
</ul>
<h3 id="heading-when-can-you-violate-yagni"><strong>When can you violate YAGNI?</strong></h3>
<ul>
<li><p>When you know a future requirement with high certainty.</p>
</li>
<li><p>When changing things in future takes considerably more effort than doing it now.</p>
</li>
</ul>
<p>Final Thoughts</p>
<p>Like already said before, these are principles - not strict laws. They’re tools to help you write better software.</p>
<p>SOLID helps you avoid fragility and rigidity</p>
<p>DRY helps you avoid redundancy</p>
<p>KISS helps you avoid complexity</p>
<p>Try to understand them. Apply them when they make sense. Be cautious when breaking them.</p>
]]></content:encoded></item><item><title><![CDATA[Why is OOP criticized?]]></title><description><![CDATA[We have previously talked about Object-Oriented Programming when discussing about why so many programming languages exist in this article:
Why are there so many programming languages?
That it was originally introduced in CPP and gradually other langu...]]></description><link>https://blog.suryasathi.com/why-is-oop-criticized</link><guid isPermaLink="true">https://blog.suryasathi.com/why-is-oop-criticized</guid><category><![CDATA[OOPS]]></category><category><![CDATA[oop]]></category><dc:creator><![CDATA[Surya Sathi]]></dc:creator><pubDate>Wed, 13 Aug 2025 06:30:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1765778889159/0dadb751-545d-4871-8b14-2911c84086f0.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>We have previously talked about Object-Oriented Programming when discussing about why so many programming languages exist in this article:</p>
<p><a target="_blank" href="https://hashnode.com/post/cmj6q0d3r000102l86dgq2x41">Why are there so many programming languages?</a></p>
<p>That it was originally introduced in CPP and gradually other languages included it as well, as a feature. We have briefly gone through what it means - OOP is a design where you write code similar to how you view real world. Let us go into a bit more detail here before going to why people criticize it.</p>
<h2 id="heading-object-oriented-programming"><strong>Object-Oriented Programming</strong></h2>
<p>OOP is a way to mimic the real-world entities, their state and their behavior.</p>
<p>If you want to code about a car, instead of writing like this:</p>
<pre><code class="lang-python">car1 = {
    color: <span class="hljs-string">'red'</span>,
    brand: <span class="hljs-string">'Toyota'</span>,
    speed: <span class="hljs-number">40</span>
}

car2 = {
    color: <span class="hljs-string">'blue'</span>,
    brand: <span class="hljs-string">'Toyota'</span>,
    speed: <span class="hljs-number">60</span>
}

car3 = {
    color: <span class="hljs-string">'white'</span>,
    brand: <span class="hljs-string">'Tata'</span>,
    speed: <span class="hljs-number">50</span>
}

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">change_color</span>(<span class="hljs-params">car, target_color</span>):</span>
    car[<span class="hljs-string">'color'</span>] = target_color

change_color(car1, <span class="hljs-string">'white'</span>)
change_color(car2, <span class="hljs-string">'red'</span>)

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">increase_speed</span>(<span class="hljs-params">car, increment</span>):</span>
    car[<span class="hljs-string">'speed'</span>] = car[<span class="hljs-string">'speed'</span>] + increment

increase_speed(car1, <span class="hljs-number">1</span>)
increase_speed(car2, <span class="hljs-number">2</span>)

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">brake</span>(<span class="hljs-params">car</span>):</span>
    car[<span class="hljs-string">'speed'</span>] = <span class="hljs-number">0</span>

brake(car1)
brake(car2)
</code></pre>
<p>You can make a base class called Car, which is like a blueprint, which will take color and brand as the states when you define it and give you an object i.e., an instance of the class (or blueprint) and you can access that object's properties when required.</p>
<pre><code class="lang-python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Car</span>:</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">init</span>(<span class="hljs-params">self, color, brand</span>):</span>
        self.color = color
        self.brand =  brand
        self.__speed = <span class="hljs-number">0</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">change_color</span>(<span class="hljs-params">self, color</span>):</span>
        self.color = color

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">increase_speed</span>(<span class="hljs-params">self, increment</span>):</span>
        self.__speed = self.__speed + increment

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">decrease_speed</span>(<span class="hljs-params">self, decrement</span>):</span>
        self.__speed = self.__speed - decrement

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">brake</span>(<span class="hljs-params">self</span>):</span>
        self.__speed = <span class="hljs-number">0</span>

car1 = Car(<span class="hljs-string">'red'</span>, <span class="hljs-string">'Toyota'</span>)
car2 = Car(<span class="hljs-string">'blue'</span>, <span class="hljs-string">'Toyota'</span>)
car3 = Car(<span class="hljs-string">'white'</span>, <span class="hljs-string">'Tata'</span>)

<span class="hljs-comment"># when you need to access a property, say, car1's color, you just do car1.color</span>
<span class="hljs-comment"># when you need to access a behavior, you can just do:</span>

car1.change_color(<span class="hljs-string">'white'</span>)
car1.increase_speed(<span class="hljs-number">2</span>)

car1.brake()
car2.brake()
</code></pre>
<p>This makes the code more readable and much more intuitive. Now OOP also has some concepts you will need to know to use it properly.</p>
<h3 id="heading-abstraction"><strong>Abstraction</strong></h3>
<p>Abstraction means providing a simplified, high-level view of an object, exposing only what's relevant to the user and while hiding the unnecessary internal details and complex implementations. It's closely related to another concept of OOP, called encapsulation.</p>
<h3 id="heading-encapsulation"><strong>Encapsulation</strong></h3>
<p>Encapsulation also means hiding the internal details of how an object works and only exposing what's necessary for other parts of the program to interact with it. This protects the data from accidental external modification. What is the difference between abstraction and encapsulation?</p>
<p>Abstraction is a design, and encapsulation is its implementation. Encapsulation tells us how exactly you can implement abstraction in the program. Abstraction is a concept while encapsulation is a mechanism.</p>
<p>In the above example, __speed is encapsulated. It is a private attribute and other parts of the code can't access it or modify it directly, outside of the Car class. We only provide them with increase_speed, decrease_speed and break methods to modify the __speed property in a controlled manner.</p>
<h3 id="heading-inheritance"><strong>Inheritance</strong></h3>
<p>Imagine you have a basic Vehicle blueprint. It has wheels, an engine, and can move. Now, you want to create a Car and a Truck. Instead of starting from scratch, you can say, "A Car is a type of Vehicle," and "A Truck is also a type of Vehicle." They can inherit the common features of a Vehicle and then add their own specific characteristics and behaviors.</p>
<p>Inheritance allows a new class (called the child or derived class) to inherit attributes and methods from an existing class (called the parent or base class). This promotes code reusability and establishes a "is-a" relationship (e.g., a "Car is a Vehicle").</p>
<p><strong>Example:</strong></p>
<pre><code class="lang-python"><span class="hljs-comment"># Parent class</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Vehicle</span>:</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">init</span>(<span class="hljs-params">self, make, model</span>):</span>
        self.make = make
        self.model = model

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">start_engine</span>(<span class="hljs-params">self</span>):</span>
        print(<span class="hljs-string">f"The <span class="hljs-subst">{self.make}</span> <span class="hljs-subst">{self.model}</span>'s engine is starting."</span>)

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">stop_engine</span>(<span class="hljs-params">self</span>):</span>
        print(<span class="hljs-string">f"The <span class="hljs-subst">{self.make}</span> <span class="hljs-subst">{self.model}</span>'s engine is stopping."</span>)

<span class="hljs-comment"># Car is a child class inheriting from Vehicle</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Car</span>(<span class="hljs-params">Vehicle</span>):</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">init</span>(<span class="hljs-params">self, make, model, num_doors</span>):</span>

        <span class="hljs-comment"># Call the parent class's init method to handle make and model</span>
        super().__init__(make, model)

        <span class="hljs-comment"># Property specific to the child class</span>
        self.num_doors = num_doors

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">drive</span>(<span class="hljs-params">self</span>):</span>
        print(<span class="hljs-string">f"The <span class="hljs-subst">{self.make}</span> <span class="hljs-subst">{self.model}</span> with <span class="hljs-subst">{self.num_doors}</span> doors is driving."</span>)

<span class="hljs-comment"># Truck is another child class inheriting from Vehicle</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Truck</span>(<span class="hljs-params">Vehicle</span>):</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">init</span>(<span class="hljs-params">self, make, model, bed_capacity</span>):</span>
        super().__init__(make, model)
        self.bed_capacity = bed_capacity

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">haul_cargo</span>(<span class="hljs-params">self</span>):</span>
        print(<span class="hljs-string">f"The <span class="hljs-subst">{self.make}</span> <span class="hljs-subst">{self.model}</span> with <span class="hljs-subst">{self.bed_capacity}</span> capacity is hauling cargo."</span>)

my_car = Car(<span class="hljs-string">"Tesla"</span>, <span class="hljs-string">"Model 3"</span>, <span class="hljs-number">4</span>)
my_truck = Truck(<span class="hljs-string">"Ford"</span>, <span class="hljs-string">"F-150"</span>, <span class="hljs-string">"1000 lbs"</span>)

my_car.start_engine() <span class="hljs-comment"># Inherited from Vehicle</span>
my_car.drive()      <span class="hljs-comment"># Specific to Car</span>

my_truck.start_engine() <span class="hljs-comment"># Inherited from Vehicle</span>
my_truck.haul_cargo()   <span class="hljs-comment"># Specific to Truck</span>
</code></pre>
<h3 id="heading-polymorphism"><strong>Polymorphism</strong></h3>
<blockquote>
<p>The word "polymorphism" comes from Greek. It means "many forms".</p>
</blockquote>
<p>Polymorphism means objects of different classes can be treated as objects of a common type. Or, put simply, you can have a single way of doing something, and different objects will respond in their own specific way.</p>
<p>Think of a "play" button on different media players. The "play" button on a music player makes music, while the "play" button on a video player plays a video. The action, pressing "play", is the same, but the outcome is different depending on the device.</p>
<p><strong>Example:</strong></p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> abc <span class="hljs-keyword">import</span> ABC, abstractmethod

<span class="hljs-comment"># An abstract base class for any kind of Media Player</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MediaPlayer</span>(<span class="hljs-params">ABC</span>):</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">init</span>(<span class="hljs-params">self, title</span>):</span>
        self.title = title
        self._is_playing = <span class="hljs-literal">False</span>

<span class="hljs-meta">    @abstractmethod</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">play</span>(<span class="hljs-params">self</span>):</span>
        <span class="hljs-keyword">pass</span>

<span class="hljs-meta">    @abstractmethod</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">pause</span>(<span class="hljs-params">self</span>):</span>
        <span class="hljs-keyword">pass</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_status</span>(<span class="hljs-params">self</span>):</span>
        <span class="hljs-keyword">return</span> <span class="hljs-string">f"<span class="hljs-subst">{self.title}</span> is <span class="hljs-subst">{<span class="hljs-string">'playing'</span> <span class="hljs-keyword">if</span> self._is_playing <span class="hljs-keyword">else</span> <span class="hljs-string">'paused'</span>}</span>."</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AudioPlayer</span>(<span class="hljs-params">MediaPlayer</span>):</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">init</span>(<span class="hljs-params">self, title, artist</span>):</span>
        super().__init__(title) <span class="hljs-comment"># Initialize the MediaPlayer part</span>
        self.artist = artist

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">play</span>(<span class="hljs-params">self</span>):</span>
        self._is_playing = <span class="hljs-literal">True</span>
        print(<span class="hljs-string">f"Playing audio: '<span class="hljs-subst">{self.title}</span>' by <span class="hljs-subst">{self.artist}</span>."</span>)
    <span class="hljs-comment"># Can have its own implementation here</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">pause</span>(<span class="hljs-params">self</span>):</span>
        self._is_playing = <span class="hljs-literal">False</span>
        print(<span class="hljs-string">f"Pausing audio: '<span class="hljs-subst">{self.title}</span>'."</span>)
    <span class="hljs-comment"># Can have its own implementation here</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">VideoPlayer</span>(<span class="hljs-params">MediaPlayer</span>):</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">init</span>(<span class="hljs-params">self, title, resolution</span>):</span>
        super().__init__(title) <span class="hljs-comment"># Initialize the MediaPlayer part</span>
        self.resolution = resolution

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">play</span>(<span class="hljs-params">self</span>):</span>
        self._is_playing = <span class="hljs-literal">True</span>
        print(<span class="hljs-string">f"Playing video in <span class="hljs-subst">{self.resolution}</span> resolution."</span>)
    <span class="hljs-comment"># Can have its own implementation here</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">pause</span>(<span class="hljs-params">self</span>):</span>
        self._is_playing = <span class="hljs-literal">False</span>
        print(<span class="hljs-string">f"Pausing video: '<span class="hljs-subst">{self.title}</span>'."</span>)
    <span class="hljs-comment"># Can have its own implementation here</span>
</code></pre>
<h3 id="heading-composition"><strong>Composition</strong></h3>
<p>Take a look at the below example code.</p>
<pre><code class="lang-python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Bird</span>:</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">fly</span>(<span class="hljs-params">self</span>):</span>
        print(<span class="hljs-string">"This bird can fly."</span>)

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">lay_eggs</span>(<span class="hljs-params">self</span>):</span>
        print(<span class="hljs-string">"This bird lays eggs."</span>)

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Eagle</span>(<span class="hljs-params">Bird</span>):</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">hunt</span>(<span class="hljs-params">self</span>):</span>
        print(<span class="hljs-string">"Eagle is hunting."</span>)

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Penguin</span>(<span class="hljs-params">Bird</span>):</span>
    <span class="hljs-comment"># Penguins can't fly, but they inherit the fly() method</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">fly</span>(<span class="hljs-params">self</span>):</span>
        print(<span class="hljs-string">"Penguins cannot fly! This is awkward."</span>)

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">swim</span>(<span class="hljs-params">self</span>):</span>
        print(<span class="hljs-string">"Penguin is swimming."</span>)

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Ostrich</span>(<span class="hljs-params">Bird</span>):</span>
    <span class="hljs-comment"># Ostriches can't fly either</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">fly</span>(<span class="hljs-params">self</span>):</span>
        print(<span class="hljs-string">"Ostrich cannot fly! I'm a runner."</span>)

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">run_fast</span>(<span class="hljs-params">self</span>):</span>
        print(<span class="hljs-string">"Ostrich is running very fast."</span>)

eagle = Eagle()
penguin = Penguin()
ostrich = Ostrich()

eagle.fly()
eagle.hunt()

penguin.fly() <span class="hljs-comment"># We had to override 'fly' just to say it can't fly.</span>
penguin.swim()

ostrich.fly() <span class="hljs-comment"># Same problem here.</span>
ostrich.run_fast()
</code></pre>
<p><strong>What's the problem here?</strong></p>
<p>The Bird class has a fly() method because most birds fly. But then we have Penguin and Ostrich which are birds, but they cannot fly. We're forced to override the fly() method in their classes just to state that they can't do what their parent class implies they can. It indicates a design flaw: not all Bird objects can fly(), so fly() shouldn't be a core behavior of Bird.</p>
<p>Now with composition we do the following. Instead of saying "A Penguin is a Bird that can fly (but actually can't)", let's think about what capabilities an animal has.</p>
<p>We can define different "behaviors" as separate, smaller objects.</p>
<pre><code class="lang-python"><span class="hljs-comment"># Define behaviors as separate classes</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CanFly</span>:</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">fly</span>(<span class="hljs-params">self</span>):</span>
        print(<span class="hljs-string">"This animal can fly by flapping wings."</span>)

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CannotFly</span>:</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">fly</span>(<span class="hljs-params">self</span>):</span>
        print(<span class="hljs-string">"This animal cannot fly."</span>)

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CanSwim</span>:</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">swim</span>(<span class="hljs-params">self</span>):</span>
        print(<span class="hljs-string">"This animal is swimming."</span>)

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CanLayEggs</span>:</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">lay_eggs</span>(<span class="hljs-params">self</span>):</span>
        print(<span class="hljs-string">"This animal lays eggs."</span>)

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CanHunt</span>:</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">hunt</span>(<span class="hljs-params">self</span>):</span>
        print(<span class="hljs-string">"This animal is hunting prey."</span>)

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CanRunFast</span>:</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">run_fast</span>(<span class="hljs-params">self</span>):</span>
        print(<span class="hljs-string">"This animal can run very fast."</span>)

<span class="hljs-comment"># Now, let's build our birds by "composing" these behaviors</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Eagle</span>:</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">init</span>(<span class="hljs-params">self</span>):</span>
        self.flying_behavior = CanFly()
        self.hunting_behavior = CanHunt()
        self.egg_laying_behavior = CanLayEggs()

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">fly</span>(<span class="hljs-params">self</span>):</span>
        self.flying_behavior.fly()

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">hunt</span>(<span class="hljs-params">self</span>):</span>
        self.hunting_behavior.hunt()

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">lay_eggs</span>(<span class="hljs-params">self</span>):</span>
        self.egg_laying_behavior.lay_eggs()

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Penguin</span>:</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">init</span>(<span class="hljs-params">self</span>):</span>
        self.flying_behavior = CannotFly()
        self.swimming_behavior = CanSwim()
        self.egg_laying_behavior = CanLayEggs()

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">fly</span>(<span class="hljs-params">self</span>):</span>
        self.flying_behavior.fly()

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">swim</span>(<span class="hljs-params">self</span>):</span>
        self.swimming_behavior.swim()

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">lay_eggs</span>(<span class="hljs-params">self</span>):</span>
        self.egg_laying_behavior.lay_eggs()

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Ostrich</span>:</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">init</span>(<span class="hljs-params">self</span>):</span>
        self.flying_behavior = CannotFly()
        self.running_behavior = CanRunFast()
        self.egg_laying_behavior = CanLayEggs()

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">fly</span>(<span class="hljs-params">self</span>):</span>
        self.flying_behavior.fly()

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">run_fast</span>(<span class="hljs-params">self</span>):</span>
        self.running_behavior.run_fast()

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">lay_eggs</span>(<span class="hljs-params">self</span>):</span>
        self.egg_laying_behavior.lay_eggs()

eagle = Eagle()
penguin = Penguin()
ostrich = Ostrich()

eagle.fly()
eagle.hunt()
eagle.lay_eggs()

penguin.fly() <span class="hljs-comment"># This correctly says it cannot fly</span>
penguin.swim()
penguin.lay_eggs()

ostrich.fly() <span class="hljs-comment"># This correctly says it cannot fly</span>
ostrich.run_fast()
ostrich.lay_eggs()
</code></pre>
<p><strong>Why is this better?</strong></p>
<p>We can easily create new types of birds by mixing and matching these behavior objects. Want a flying penguin? Just give it <em>CanFly</em> behavior! You don't have to redefine an entire inheritance tree. Penguin and Ostrich no longer "pretend" to fly and then awkwardly tell you they can't. Their "flying" behavior is explicitly defined as <em>CannotFly</em>. And if we ever change how <em>CanFly</em> works, it only affects objects that have a <em>CanFly</em> object, not every class in a deep inheritance hierarchy.</p>
<p>This is the essence of composition: instead of inheriting features from a parent class, you build your objects by including instances of other objects that provide the desired behaviors. You're giving the object "parts" that define what it can do, rather than inheriting a whole "blueprint" that might contain unwanted features.</p>
<h2 id="heading-things-to-be-careful-about"><strong>Things to be careful about</strong></h2>
<p>While there is a concept of static methods, which are like utility methods in a class, which can't access an object's state directly, you will notice that a vast majority of methods you write in a class often directly deal with some state of an object. After all, the fundamental idea of OOP is to encapsulate the state and behaviors into a self-contained unit, an object. This core philosophy by itself is where <strong>functional programming</strong> differs. But that's a topic for another time.</p>
<p>Meanwhile, you will notice that I have said that OOP lets you write a more readable and intuitive code. But that doesn't mean if you are using OOP, you are writing great code. You will find many people criticizing OOP.</p>
<p>One of the key reasons highlighted by many is exposure to bad usage of OOP in the code bases which made them hate it. <strong>Endless inheritance</strong> trees making it difficult to understand the code (ironic when OOP is made so code can be intuitive), <strong>fragile base class</strong> problem (meaning same base class being used by too many different classes - so when you need to make a change to base class in the future, you will have to go through all the child classes to make sure nothing breaks. Though this is what unit testing is for), <strong>uncontrolled state changes</strong> (meaning different parts of your program can affect your state making it hard to track how it will change), <strong>over engineering</strong> (using classes and objects where not necessary) along with some other reasons lead to the increased disillusionment with OOP.</p>
<p>So you can avoid that by properly understanding when and how to use OOP:</p>
<ol>
<li><p>Try to model your classes based on intuitive entities.</p>
</li>
<li><p>Use it only when necessary. Not everything needs to be a class. Sometimes a simple function will do the job.</p>
</li>
<li><p>Proper encapsulation.</p>
</li>
<li><p>Use small classes with single responsibility instead of a single class with too many methods.</p>
</li>
<li><p>Try to use composition over inheritance when useful</p>
</li>
</ol>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>You don't need to use every design principle of OOP in your code just because you can. Based on the problem you are dealing with, if only a few features of OOP can do the job, then use just those. Always look for simplest, cleanest and efficient answer. When you don't need composition, don't use it. When you don't need inheritance, don't use it. Sometimes you may not even need OOP, just a regular function might do. Then don't use it.</p>
]]></content:encoded></item><item><title><![CDATA[Asynchronous Programming in Python and NodeJS]]></title><description><![CDATA[In the last article, we have talked about concurrency and parallelism at OS level. Concurrency is when tasks can start and run seemingly at the same time. Parallelism is when tasks actually run at the same time. Now let's talk about how you can acces...]]></description><link>https://blog.suryasathi.com/asynchronous-programming-in-python-and-nodejs</link><guid isPermaLink="true">https://blog.suryasathi.com/asynchronous-programming-in-python-and-nodejs</guid><category><![CDATA[asynchronous]]></category><category><![CDATA[asynchronous programming]]></category><category><![CDATA[asynchronous JavaScript]]></category><category><![CDATA[async/await]]></category><category><![CDATA[Python 3]]></category><category><![CDATA[JavaScript]]></category><dc:creator><![CDATA[Surya Sathi]]></dc:creator><pubDate>Wed, 06 Aug 2025 06:30:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1765778613535/41361ed9-37b8-4c04-9cd5-582e90288fc5.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the last article, we have talked about concurrency and parallelism at OS level. Concurrency is when tasks can start and run seemingly at the same time. Parallelism is when tasks actually run at the same time. Now let's talk about how you can access them at a language level, which will also let you see, that different languages have different ways of implementing these features due to the different core philosophies and how they've been designed. We will go through how Python and NodeJS implements them in this article. Before that let us go through the foundational concept, asynchronous programming.</p>
<h3 id="heading-asynchronous-programming-when-youre-waiting"><strong>Asynchronous Programming: When You're Waiting</strong></h3>
<p>Now, let's talk about event loops and asynchronous programming. Imagine you're a coffee shop manager. When a customer orders a coffee, they have to wait for it to be prepared. But you don't wait with them—you take other orders for others in the meantime, and when the coffee is prepared, you give it to the customer.</p>
<p>This "doing something else while waiting" is the essence of asynchronous programming. When you write an application, many operations involve waiting: waiting for data from a network, waiting for a file to be read from disk, waiting for a database query to return results. These are called I/O-bound tasks (Input/Output bound).</p>
<p>This is where asynchronous programming comes into play. You typically write code using the async/await syntax.</p>
<p>At its core, this concept uses an <strong>event loop</strong>.</p>
<h3 id="heading-event-loop"><strong>Event Loop</strong></h3>
<p>Think of the event loop as the coffee shop manager in our example. When you place an order (start an I/O operation), the manager notes it down and goes to the next customer. When your coffee is ready, a signal is sent to the manager, who then comes back to you.</p>
<p>In asynchronous programming, when your code encounters an await keyword, it means "I'm going to start an I/O operation here, and while I wait for it to complete, the event loop can go off and do other tasks." When that I/O operation finishes, the event loop then comes back to where you awaited and resumes your code. This way, a single thread can manage many concurrent I/O operations without getting blocked. It's like a single barista handling multiple orders efficiently by constantly switching tasks.</p>
<p>So, while threads are about potentially running multiple things in parallel, <strong>event loops are about running many I/O-bound tasks concurrently within a single thread</strong>. It's about maximizing the utilization of that single thread by not letting it sit idle during waiting periods.</p>
<p><strong>Examples:</strong></p>
<p><strong>Python:</strong></p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> asyncio

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">fetch_data</span>():</span> <span class="hljs-comment"># declares a coroutine (an async function)</span>
    print(<span class="hljs-string">"Start fetching..."</span>)

    <span class="hljs-comment"># Simulate I/O: pauses for 2 seconds. Meanwhile other tasks continue.</span>
    <span class="hljs-keyword">await</span> asyncio.sleep(<span class="hljs-number">2</span>)

    print(<span class="hljs-string">"Done fetching!"</span>)

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">main</span>():</span>
    <span class="hljs-comment"># runs them seemingly at the same time</span>
    <span class="hljs-keyword">await</span> asyncio.gather(fetch_data(), fetch_data())

asyncio.run(main()) <span class="hljs-comment"># starts the event loop and runs the main coroutine</span>
</code></pre>
<p><strong>NodeJS:</strong></p>
<pre><code class="lang-python">const fs = require(<span class="hljs-string">'fs/promises'</span>);

// declares an asynchronous function

<span class="hljs-keyword">async</span> function readFile() {
    console.log(<span class="hljs-string">"Reading file..."</span>);

    // the event loop continues handling other tasks <span class="hljs-keyword">while</span> this waits.
    const content = <span class="hljs-keyword">await</span> fs.readFile(<span class="hljs-string">'example.txt'</span>, <span class="hljs-string">'utf8'</span>);

    console.log(<span class="hljs-string">"File content:"</span>, content);
}

readFile();
readFile();
</code></pre>
<h2 id="heading-python"><strong>Python</strong></h2>
<p>Before we jump ahead, we absolutely have to talk about something called the Global Interpreter Lock, or GIL.</p>
<h3 id="heading-global-interpreter-lock-gil"><strong>Global Interpreter Lock (GIL)</strong></h3>
<p>GIL is a locking mechanism which makes sure that only one thread can enter and execute Python bytecode at any given time.</p>
<p>Now, why does Python have this GIL? Well, it's mainly there to make memory management simpler and safer. Without it, imagine multiple threads trying to modify the same piece of data in memory simultaneously—it could lead to unpredictable and hard-to-debug issues. The GIL prevents these kinds of race conditions.</p>
<p>However, the downside is that even if you have a powerful multi-core processor, the GIL prevents multiple threads from executing Python bytecode in parallel. This means if your program is heavily dependent on CPU-bound tasks (tasks that spend most of their time crunching numbers), then using multiple threads in Python won't magically make it run faster across all your CPU cores.</p>
<p>But GIL only applies to Python bytecode. If your code calls out to underlying C libraries (which many popular Python libraries do, like NumPy for numerical computations), then the GIL can be released, allowing those C operations to run in parallel. This is a crucial point to remember!</p>
<h3 id="heading-concurrency-threading"><strong>Concurrency: Threading</strong></h3>
<p>Now with GIL in place, how does threading work in Python? Python's threading module allows you to create and manage multiple threads within a single process.</p>
<p>When you use threads in Python, you are indeed achieving concurrency. Multiple threads can exist and execute seemingly at the same time. However, due to the GIL, only one thread can be executing Python bytecode at any given moment. The operating system's scheduler rapidly switches between these threads, giving the illusion of parallel execution.</p>
<p>So, when is threading useful in Python?</p>
<p>I/O-bound tasks: If your task involves a lot of waiting (like fetching data from the internet, reading a large file, or waiting for a database response), then threads can be very effective. While one thread is waiting for an I/O operation to complete, the GIL is released, allowing another thread to execute Python bytecode. This means you're still making progress on other parts of your program.</p>
<p>Tasks that release the GIL: As mentioned earlier, if your code calls out to underlying C libraries that release the GIL (like many numerical processing libraries), then you can truly achieve parallelism with threads for those specific operations.</p>
<p>However, if your task is CPU-bound (e.g., heavy mathematical calculations, image processing, complex algorithms that primarily involve Python code), then using multiple threads in Python might not give you the performance boost you expect. In fact, the overhead of managing threads and the GIL switching can sometimes even make CPU-bound threaded programs slower than their single-threaded counterparts.</p>
<p>It’s also important to note that not all libraries behave the same way with regard to GIL. Just because a library is implemented in C doesn’t guarantee it will release the GIL.</p>
<p><strong>Example:</strong></p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> threading
<span class="hljs-keyword">import</span> time

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">download_file</span>():</span>
    print(<span class="hljs-string">"Start downloading..."</span>)
    time.sleep(<span class="hljs-number">3</span>)  <span class="hljs-comment"># Blocking, simulates I/O</span>
    print(<span class="hljs-string">"Download complete!"</span>)

<span class="hljs-comment"># Start two threads to run the download_file function</span>
t1 = threading.Thread(target=download_file)
t2 = threading.Thread(target=download_file)


<span class="hljs-comment"># runs both functions concurrently.</span>
t1.start()
t2.start()

<span class="hljs-comment"># waits for both threads to finish</span>
t1.join()
t2.join()

print(<span class="hljs-string">"Both downloads finished."</span>)
</code></pre>
<h3 id="heading-parallelism-multiprocessing"><strong>Parallelism: Multiprocessing</strong></h3>
<p>If you want to achieve true parallelism for CPU-bound tasks in Python, where different parts of your program run simultaneously on different CPU cores, then the multiprocessing module is your choice.</p>
<p>Multiprocessing, as the name implies, creates separate processes instead of threads. Think of each process as a completely independent instance of the Python interpreter, with its own memory space and its own GIL. Because each process has its own GIL, they can all run simultaneously on different CPU cores without interference.</p>
<p>The multiprocessing module handles all the complexities of creating these processes and provides ways for them to communicate with each other (if needed), for example, through queues or pipes.</p>
<p>That said, using multiprocessing has trade-offs: processes are heavier than threads and inter-process communication can be slower than shared-memory in concurrency.</p>
<p><strong>Example:</strong></p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> multiprocessing <span class="hljs-keyword">import</span> Process
<span class="hljs-keyword">import</span> time

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">heavy_computation</span>():</span>
    print(<span class="hljs-string">"Start computing..."</span>)
    total = <span class="hljs-number">0</span>

    <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(<span class="hljs-number">10</span>**<span class="hljs-number">7</span>):  <span class="hljs-comment"># CPU-intensive loop</span>
        total += i
    print(<span class="hljs-string">"Computation done:"</span>, total)

<span class="hljs-comment"># Creates two new OS processes. They truly run in parallel on different CPU cores.</span>
p1 = Process(target=heavy_computation)
p2 = Process(target=heavy_computation)

p1.start()
p2.start()

p1.join()
p2.join()

print(<span class="hljs-string">"Both computations finished."</span>)
</code></pre>
<h2 id="heading-nodejs"><strong>NodeJS</strong></h2>
<p>Node JS achieves the same concepts of concurrency and parallelism quite differently from Python. To know why, you should first get to know the main philosophy of NodeJS.</p>
<h3 id="heading-single-threaded-nature-of-nodejs"><strong>Single-Threaded Nature of NodeJS</strong></h3>
<p>The most fundamental concept to grasp about NodeJS is that its main execution thread is single-threaded. This means that your JavaScript code runs on one single thread. There's no GIL here in the Python sense because JavaScript itself doesn't have a GIL; it's designed differently.</p>
<p>Then how can it handle so many users and tasks if it's single-threaded? This is where the event loop with non-blocking I/O come in.</p>
<h3 id="heading-concurrency-with-non-blocking-io"><strong>Concurrency: With Non-Blocking I/O</strong></h3>
<p>Node.js is designed with non-blocking I/O. This means when your Node.js code needs to do something that takes time, like reading a file from disk, making a request to another server, or fetching data from a database—instead of waiting, Node.js tells the OS or some background libraries, "Hey, can you do this for me? When you're done, let me know!" And then, it immediately moves on to the next task.</p>
<p>Those background libraries themselves might use multiple threads.</p>
<p><strong>Simplified Flow</strong></p>
<p>When your Node.js application starts, the event loop begins its cycle.</p>
<p>It executes any synchronous JavaScript code first (like setting up variables or simple calculations). When it encounters a file system asynchronous operation like reading a file, it offloads that task to an underlying C++ library called libuv. But when it encounters a network I/O task, it uses OS async APIs.</p>
<p>Libuv maintains a thread pool, a small group of actual operating system threads, to handle these heavy, blocking I/O tasks. So, when your Node.js code says "read this file," libuv picks an available thread from its pool to do the actual file reading. Once that background thread finishes the I/O operation, it doesn't return the result directly. Instead, it places a callback function—a piece of code that should run when the task is complete—into an <strong>event queue</strong>.</p>
<p>The event loop continuously checks if the <strong>call stack</strong> (where synchronous code runs) is empty. If it is, it picks the next callback from the event queue and pushes it onto the call stack for execution.</p>
<p>This continuous cycle ensures that the main JavaScript thread is almost always busy doing something useful, either processing new requests or handling the results of completed asynchronous operations, instead of waiting idly.</p>
<p>This model is incredibly efficient for I/O-bound applications (like web servers) because the single JavaScript thread is never blocked waiting for slow I/O operations. It's always free to accept new connections and process other requests.</p>
<p><strong>Example:</strong></p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> fs = <span class="hljs-built_in">require</span>(<span class="hljs-string">'fs'</span>);

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">readFileAsync</span>(<span class="hljs-params">filename</span>) </span>{

<span class="hljs-comment">// fs.readFile is non-blocking — it doesn't wait for the result. The event loop continues running after calling both reads. The callbacks are triggered once each file is read.</span>

  fs.readFile(filename, <span class="hljs-string">'utf8'</span>, <span class="hljs-function">(<span class="hljs-params">err, data</span>) =&gt;</span> {
    <span class="hljs-keyword">if</span> (err) <span class="hljs-keyword">throw</span> err;
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Read <span class="hljs-subst">${filename}</span>:`</span>, data);
  });
}

readFileAsync(<span class="hljs-string">'file1.txt'</span>);
readFileAsync(<span class="hljs-string">'file2.txt'</span>);

<span class="hljs-comment">// Node doesn’t create OS-level threads here — it offloads to libuv's internal mechanisms.</span>

<span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Files are being read..."</span>);
</code></pre>
<h3 id="heading-parallelism-with-worker-threads"><strong>Parallelism: With Worker Threads</strong></h3>
<p>While Node.js's single-threaded nature simplified programming (no need to worry about shared memory, race conditions), this model had a limitation: <strong>if you had a CPU-bound task, it would block the entire Event Loop</strong>. In our previous flow, by the time the I/O task is complete and a callback function is set in the event queue, if the main execution stack still keeps running for a long time, this callback will not get executed until the call stack is free.</p>
<p>To address this, <strong>worker threads</strong> were introduced. Worker threads allow you to create separate, isolated JavaScript execution environments that run on different threads. Each worker thread has its own V8 engine instance, its own event loop, and its own memory space.</p>
<p>This means you can offload computationally intensive tasks to a worker thread, keeping the main thread (and its event loop) free and responsive to handle incoming requests. The worker thread will have its own event loop, but it will still be in the same process. The benefit is that the main thread won't be blocked by the long running heavy computation happening in the worker thread.</p>
<p>That said, worker threads come with their own complexities. They are heavier than you might expect (each runs its own V8 instance), and communication between threads happens via message passing.</p>
<p><strong>Example:</strong></p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> { Worker, isMainThread, parentPort } = <span class="hljs-built_in">require</span>(<span class="hljs-string">'worker_threads'</span>);

<span class="hljs-keyword">if</span> (isMainThread) {
  <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">runWorker</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function">(<span class="hljs-params">resolve, reject</span>) =&gt;</span> {
      <span class="hljs-keyword">const</span> worker = <span class="hljs-keyword">new</span> Worker(__filename);
      worker.on(<span class="hljs-string">'message'</span>, resolve);
      worker.on(<span class="hljs-string">'error'</span>, reject);
    });
  }

  runWorker().then(<span class="hljs-function"><span class="hljs-params">result</span> =&gt;</span> {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Worker result:"</span>, result);
  });

} <span class="hljs-keyword">else</span> {
  <span class="hljs-keyword">let</span> total = <span class="hljs-number">0</span>;
  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">100</span>; i++) 
    total += i;
  }

  parentPort.postMessage(total);
}
</code></pre>
<h3 id="heading-parallelism-with-clustering"><strong>Parallelism: With Clustering</strong></h3>
<p>In addition to worker threads, Node.js also supports another technique for achieving parallelism across CPU cores: clustering.</p>
<p>The <strong>cluster</strong> module in Node.js allows you to spawn multiple child processes, each running an instance of your server. These child processes share the same server port and can handle incoming requests in parallel. Essentially, you’re replicating the same Node.js process multiple times, with each process operating independently and utilizing a different CPU core.</p>
<p>Imagine you have an 8-core machine. With clustering, you can start 8 separate Node.js processes (workers), all managed by a master process. When a new connection is received, the master process can distribute it to one of the worker processes, often using a round-robin or OS-level load balancing strategy.</p>
<p>This approach provides true parallelism at the process level without changing your application code much. It’s especially useful for scaling I/O-bound web applications horizontally within a single machine.</p>
<p>However, like multiprocessing in Python, clustering comes with its own caveats: each worker has its own memory space, so sharing data across workers requires inter-process communication via messaging or external data stores.</p>
<p><strong>Example:</strong></p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> cluster = <span class="hljs-built_in">require</span>(<span class="hljs-string">'cluster'</span>);
<span class="hljs-keyword">const</span> http = <span class="hljs-built_in">require</span>(<span class="hljs-string">'http'</span>);
<span class="hljs-keyword">const</span> os = <span class="hljs-built_in">require</span>(<span class="hljs-string">'os'</span>);

<span class="hljs-keyword">if</span> (cluster.isMaster) {
  <span class="hljs-keyword">const</span> numCPUs = os.cpus().length;
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Master PID <span class="hljs-subst">${process.pid}</span>, spawning <span class="hljs-subst">${numCPUs}</span> workers`</span>);

  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">0</span>; i &lt; numCPUs; i++) {
    cluster.fork(); <span class="hljs-comment">// Spawn worker</span>
  }

} <span class="hljs-keyword">else</span> {
  http.createServer(<span class="hljs-function">(<span class="hljs-params">req, res</span>) =&gt;</span> {
    res.end(<span class="hljs-string">`Handled by PID <span class="hljs-subst">${process.pid}</span>`</span>);
  }).listen(<span class="hljs-number">3000</span>);
}
</code></pre>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>It's crucial to understand the strengths and limitations of different programming languages and use them accordingly. You might choose Node.js for building a web server due to its efficient non-blocking I/O model, which is great handling high-concurrency I/O-bound workloads, such as handling multiple HTTP requests, reading/writing to databases, or working with file systems.</p>
<p>However, when it comes to CPU-bound or computation-heavy tasks like data processing, image manipulation, or machine learning, Node.js is not ideal due to its single-threaded event loop. In such cases, it's often better to offload these tasks to a separate service written in a language more suited for heavy computation, such as Python or C++.</p>
<p>If you're using Python, you may worry about the GIL restricting parallelism in multithreaded programs. While it's true that the GIL limits CPU-bound concurrency in threads, I/O-bound concurrency (e.g. network calls, disk I/O) can still benefit from multithreading. For CPU-bound tasks, using multiprocessing, which spawns separate processes, is often the better approach.</p>
<p>More importantly, you can combine both NodeJS and Python to play to each of their strengths — using Node.js for handling high-throughput asynchronous I/O operations, and Python for offloading compute-heavy tasks and leveraging its rich ecosystem in data science, AI, and numerical computing.</p>
]]></content:encoded></item><item><title><![CDATA[How Your OS Runs Applications]]></title><description><![CDATA[When you use your computer—whether for browsing, gaming, coding, or anything else—everything ultimately comes down to calculations and instructions being carried out. The part that does all the actual calculations and follows every single instruction...]]></description><link>https://blog.suryasathi.com/how-your-os-runs-applications</link><guid isPermaLink="true">https://blog.suryasathi.com/how-your-os-runs-applications</guid><category><![CDATA[cpu]]></category><category><![CDATA[Threading]]></category><category><![CDATA[processes]]></category><category><![CDATA[64-bit processor]]></category><category><![CDATA[32-bit processor]]></category><category><![CDATA[multitasking]]></category><category><![CDATA[os]]></category><category><![CDATA[cores]]></category><category><![CDATA[context switching]]></category><category><![CDATA[time sharing]]></category><dc:creator><![CDATA[Surya Sathi]]></dc:creator><pubDate>Wed, 30 Jul 2025 06:30:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1765778281663/48d3d9e0-baab-42be-b5b2-d61b65498058.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When you use your computer—whether for browsing, gaming, coding, or anything else—everything ultimately comes down to calculations and instructions being carried out. The part that does all the actual calculations and follows every single instruction is the Central Processing Unit, or <strong>CPU</strong>. You can think of the CPU as the entire brain chip of your computer, tirelessly handling every task.</p>
<h3 id="heading-cpu"><strong>CPU</strong></h3>
<h3 id="heading-cores-workers-of-cpu"><strong>Cores: Workers of CPU</strong></h3>
<p>Inside this CPU chip, there are typically multiple independent working units called <strong>cores</strong>. Each core is like an independent worker. The crucial point is that each individual CPU core can execute instructions from only one single task at any given instant. Even if a core is incredibly fast, it processes one instruction after another, one at a time.</p>
<p>So, a CPU with four cores can perform four distinct sets of instructions <strong>simultaneously</strong>, one on each core.</p>
<p>Generally, in most modern personal computers you'd buy today – like laptops or desktop PCs – you'll commonly find CPUs with 4 to 8 cores. High-end consumer computers or specialized workstations might have 10, 12, or even 16 cores. <strong>Servers</strong>, which are powerful computers designed for heavy tasks, can have many more, sometimes 32, 64, or even hundreds of cores across multiple CPU chips.</p>
<h3 id="heading-registers-cpus-own-memory"><strong>Registers: CPU's Own Memory</strong></h3>
<p>Each CPU core also has its own very small, super-fast storage areas right inside it, called <strong>CPU Registers</strong>. When a core is actively working on instructions, it uses these registers to hold the numbers and pieces of information it needs right at that exact moment. They are the fastest place for the core to access data it's currently manipulating.</p>
<h3 id="heading-32-bit-vs-64-bit-architecture"><strong>32-bit vs 64-bit Architecture</strong></h3>
<p>When we talk about 32-bit and 64-bit CPUs, we are referring to a fundamental aspect of their design/architecture: the size of the data chunks a CPU core can process in a single operation.</p>
<p>Performance wise, a 64-bit processor can be faster than a 32-bit one, for applications that require large memory or deal with large data types. As an example, if you add two large numbers that require 64 bits to represent, a 64-bit CPU can do that in a single operation. A 32-bit CPU, on the other hand, would need to break down a 64-bit number into two 32-bit pieces and perform multiple operations to achieve the same result, making it slower for such tasks.</p>
<h3 id="heading-impact-of-cpu-architecture-on-ram"><strong>Impact of CPU Architecture on RAM</strong></h3>
<p>Another benefit of increased bit capacity is the amount of memory a CPU can utilize. Imagine your computer's main memory (RAM) as a vast collection of tiny memory boxes, and each box has a unique "address".</p>
<p>A <strong>32-bit CPU</strong> uses 32 digital bits (which are 0s or 1s) to create these memory addresses. With 32 bits, you can create exactly 2^32 unique addresses. If each address refers to one byte of memory, then 2^32 bytes is <strong>approximately 4 Gigabytes (GB)</strong>. This means a 32-bit CPU can only directly "point to" or use a maximum of about 4 GB of your computer's main memory. Even if you install more than 4 GB of RAM, a 32-bit CPU cannot physically address or utilize that extra memory beyond its 4 GB limit.</p>
<p>However, a 64-bit CPU uses 64 bits to create memory addresses. This allows for an astronomically larger number of unique addresses – 2^64. This vastly expanded addressing capability means a <strong>64-bit CPU</strong> has the capacity to use a huge amount of RAM, easily reaching into <strong>Terabytes (TB) of RAM</strong> and beyond, limited mainly by how much RAM you can provide as hardware. This capability is essential for modern software, which often requires significant amounts of memory to run efficiently.</p>
<p>With this understanding of what the CPU can do, let’s look at how the software—particularly the operating system—takes advantage of it.</p>
<h3 id="heading-operating-system-os-the-coordinator"><strong>Operating System (OS): The Coordinator</strong></h3>
<p>The Operating System is a very special, complex program, a piece of <strong>software</strong>, that acts as the coordinator of your entire computer system. It is crucial to understand that the OS is a software, while the CPU is a hardware. They are distinct but work very closely together.</p>
<p>The OS is responsible for controlling and coordinating all the hardware components (including the CPU and its cores, input devices like keyboards, output devices like screens) and all the other software programs you run. It acts as the bridge between you, your programs, and the computer's hardware. One of its key roles is directly managing the cores of the CPU. The OS tells each individual CPU core what instructions to execute and when.</p>
<h3 id="heading-multitasking-time-sharing-and-context-switching"><strong>Multitasking: Time-Sharing and Context Switching</strong></h3>
<p>In the very early days of computers, only one program could run at a time. You had to finish one task before starting another. The ability to run multiple programs seemingly at once, called <strong>multitasking</strong>, was a major advancement implemented by the OS.</p>
<p>The OS achieves multitasking through a technique called <strong>time-sharing</strong>. A key part, known as the <strong>OS scheduler</strong>, is responsible for this. The scheduler doesn't let one program run continuously. Instead, it gives each running program's active worker (a "thread," which we'll discuss next) a very small slice of CPU time on a core.</p>
<p>After that tiny slice of time, the OS takes control back from that thread and then gives the CPU core to another thread for its own small slice of time, and so on. This switching happens incredibly fast, often thousands or millions of times per second. Because of this rapid switching, it creates the <strong>illusion that all programs are running simultaneously</strong>, even if your computer only has one CPU core.</p>
<p>How does the OS manage to take control back from a running program? It uses hardware mechanisms called interrupts. When a CPU core receives an interrupt or even when a thread voluntarily yields control, it immediately stops what it's currently doing and temporarily hands control over to the OS. This allows the OS to regain control from the running thread, even if that thread is very busy. Once the OS has control, its scheduler can then decide which thread gets to run next on that CPU core. This entire process of pausing one thread and starting another, including saving and loading their states, is called <strong>context switching</strong>.</p>
<h3 id="heading-processes-and-threads"><strong>Processes and Threads</strong></h3>
<p>A <strong>process</strong> is an <strong>isolated environment</strong> for a running program, created and managed by the OS. It includes dedicated memory, file access, network permissions, and more.</p>
<p>When you open any program on your computer, the OS creates a process for it. It also assigns and manages all the resources a process needs, such as its dedicated segment of memory, access to specific files on the hard drive, and network connections, and prepares it for execution. When you close the program, the OS cleans up and removes its process, freeing up all its resources.</p>
<p>The OS uses special hardware capabilities to enforce strict boundaries between processes. This means one process cannot directly read from or write to the memory space of another process without explicit permission from the OS. This isolation is fundamental for system stability and security; if one program crashes, its <strong>issues are contained</strong> within its own process, preventing it from corrupting or crashing other running programs or the entire operating system.</p>
<p>Inside each program (process), there can be one or many <strong>threads</strong>. A thread is a smaller, individual sequence of instructions or a specific "worker" within a process. All threads within the same process share that process's allocated memory space and resources. This makes them very efficient for tasks that need to cooperate closely and share data within one program. The OS is also responsible for creating, destroying, and managing these threads within their respective processes.</p>
<p>It is crucial to understand that processes and threads are concepts and structures created and <strong>managed entirely by the Operating System</strong>. They are not inherent features of the CPU hardware itself. The CPU simply executes the instructions that the OS tells it to run, which come from threads.</p>
<h3 id="heading-how-processes-threads-and-cores-work-together"><strong>How Processes, Threads, and Cores Work Together</strong></h3>
<p>Let’s make the relationship clear:</p>
<p><strong>Only one thread runs on a single CPU core at any given instant.</strong> This is a fundamental rule. A CPU core can execute instructions from only one thread at a time. The OS scheduler assigns a thread to a specific CPU core to execute its instructions. The OS tells a core, "Now, execute instructions from this thread."</p>
<p>A thread does not always run on the same core. When a thread is paused (during a context switch) and then later resumed, the OS scheduler can (and often does, for load balancing) choose to run that thread on <strong>any available CPU core</strong>, not necessarily the same one it was on before. This flexibility helps the OS efficiently manage the workload across all cores.</p>
<p>During context switching, the complete <strong>state</strong> of the thread being paused is saved. This includes all the values currently held in the CPU's registers for that thread, the exact instruction it was about to execute next (its <strong>program counter</strong>), and any other crucial information about its current progress. This entire state is copied from the CPU's registers and stored into a specific memory area designated for that thread, which resides in the computer's RAM. When the OS decides to resume that thread later, it retrieves this saved state from RAM and loads it back into the CPU's registers, allowing the thread to continue executing precisely from where it left off.</p>
<p>And multiple threads of the same process can run on different cores simultaneously. This is a key benefit of multi-core CPUs. If your program (process) has multiple threads, the OS scheduler can distribute those threads across different available CPU cores. This allows different parts of your single program to genuinely execute at the same moment, speeding up its overall performance.</p>
<p>This is how modern applications take full advantage of multi-core CPUs. Threads allow programs to scale their workload, and the OS ensures they are efficiently distributed across available cores.</p>
<h3 id="heading-concurrency-and-parallelism"><strong>Concurrency and Parallelism</strong></h3>
<p>Now that we understand cores, threads, and the OS's role, it's important to define two often-confused concepts, concurrency and parallelism:</p>
<p><strong>Concurrency</strong> is about the ability of the OS to manage and make progress on many tasks over time, even if there are not enough CPU cores to execute them all at the exact same instant. It relies on time-sharing and rapid context switches between various threads, creating the illusion of simultaneous execution. This is possible on any computer, even those with only one CPU core.</p>
<p><strong>Parallelism</strong> is about the actually doing of multiple tasks at the exact same moment. This is achieved when the OS schedules different threads to run on different, available CPU cores. So, if you have a 4-core CPU, the OS can run up to four distinct threads truly in parallel. This is possible only if you have a multi-core CPU.</p>
<p>Modern systems combine both: they use concurrency to manage more tasks than available cores, and parallelism to speed up those tasks on multicore hardware.</p>
<h3 id="heading-example-running-multiple-tabs-on-a-browser"><strong>Example: Running Multiple Tabs on a Browser</strong></h3>
<p>Let’s make this all even more tangible by walking through a real-world scenario: opening multiple tabs in a web browser.</p>
<p>When you launch your web browser, say Chrome, the Operating System creates a process for it. This process includes everything the browser needs: memory to hold the pages you view, access to the network for loading websites, and access to files for caching or downloads.</p>
<p>Now, when you open multiple tabs within that browser, the architecture becomes more layered:</p>
<p>Each tab is often handled by a separate process (or at least a separate thread, depending on the browser's internal design). For example, Chrome is well known for creating a separate process per tab. This isolation means that if one tab crashes due to a faulty script, it doesn’t take down other tabs or the entire browser—because the OS has enforced process-level isolation.</p>
<p>Within each browser process or tab, there are multiple threads. For instance, one thread may handle rendering the web page (graphics), another may handle JavaScript execution, while another manages network communication. All these threads share the same memory and work closely together, allowing the web page to load smoothly and respond to user interactions.</p>
<p>The OS scheduler assigns these threads to CPU cores. If your computer has four cores, four threads—possibly from four different tabs or background tasks—can run truly in parallel. If there are more threads than cores (which is usually the case), the scheduler uses time-sharing to rapidly switch threads in and out of each core.</p>
<p>Context switching ensures that if you're watching a video in one tab, loading another page in a second tab, and downloading a file in the background, your system appears responsive—even if those operations are switching in and out of a single core every few milliseconds.</p>
<h3 id="heading-conclusion"><strong>Conclusion</strong></h3>
<p>Understanding this coordination between the CPU, its cores, the OS, processes, and threads is crucial to understand how your computer gets work done. Whether you're writing code, using software, or just curious about how things work, this is how it's done.</p>
]]></content:encoded></item><item><title><![CDATA[Understanding Big O]]></title><description><![CDATA[There are often many ways to solve a problem. These different approaches are called algorithms. An algorithm is just a clear, well-defined set of step-by-step instructions that a computer follows to perform a computation. For example, if you want to ...]]></description><link>https://blog.suryasathi.com/understanding-big-o</link><guid isPermaLink="true">https://blog.suryasathi.com/understanding-big-o</guid><category><![CDATA[big o]]></category><category><![CDATA[#big o notation]]></category><category><![CDATA[Time Complexity]]></category><dc:creator><![CDATA[Surya Sathi]]></dc:creator><pubDate>Tue, 22 Jul 2025 18:30:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1765778025009/a703f9c8-e512-4e10-b41c-fbe698dfec0a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>There are often many ways to solve a problem. These different approaches are called <strong>algorithms</strong>. An algorithm is just a clear, well-defined set of step-by-step instructions that a computer follows to perform a computation. For example, if you want to find a specific word in a dictionary, you could start from the very first page and read every word until you find it. Or, you could open the dictionary roughly in the middle, see if your word is before or after, and then narrow down your search. Both are algorithms to find a word. But one is more efficient than the other.</p>
<p>This is why we look into solving the same problem using different algorithms. Because some are just more efficient than others. So, how do you figure out which one’s better? You usually look at two things: how much memory it eats up (space complexity) and how long it takes to run (time complexity). In this one, we’re just talking about time complexity.</p>
<h2 id="heading-what-is-it"><strong>What is it?</strong></h2>
<p>Big O is a way to describe this time complexity. Think of it as a way to describe how much "work" an algorithm has to do as the amount of "stuff" i.e., input size, it's working with grows. It's not about the exact number of seconds an algorithm takes, because that can change depending on how fast your computer is or what other programs are running in the background. Instead, Big O focuses on how fast the amount of work grows as the input size grows.</p>
<p>Big O helps us categorize algorithms based on how their performance scales. It gives us a kind of a "worst-case scenario" for how long an algorithm might take. We use a special notation, often with a capital 'O' followed by parentheses, like O(n) or O(n^2).</p>
<p>Let's go through some examples:</p>
<h2 id="heading-o1-constant-time"><strong>O(1): Constant Time</strong></h2>
<p>Imagine, in the above dictionary example, you already know the page number and the position of the word you are looking for. Then you don't need to go through all the words one by one. If you already know the page number is 20, it doesn't matter whether the dictionary has 100 pages or 1000 pages, you will always open page 20 as soon as you open the dictionary. This type of getting what you want in the same time no matter the input size, is O(1).</p>
<p><strong>Example:</strong></p>
<ul>
<li><p>Accessing an element in an array by its index - it doesn't matter how many elements they are, you can just jump to its index In the memory.</p>
</li>
<li><p>Adding an element at the front in a singly linked list - you are just adding an element and referencing the pointer to the previous first item and it doesn't matter how many elements there are after it.</p>
</li>
</ul>
<h2 id="heading-on-linear-time"><strong>O(n): Linear Time</strong></h2>
<p>In the same example, if you don't know the page number, then you are not just accessing the word but you are searching for it. And if you are reading every word, if you have a small dictionary with only 100 pages, in the worst case scenario, the word will be in the 100th page and you will have to read all 100 pages. If you have a 1000 pages, you will have to read all 1000 pages. That means the work grows directly with the number of pages - double the pages, double the time. So, you can say the time complexity in this case is O(n).</p>
<p><strong>Example:</strong></p>
<ul>
<li><p>Searching for an item in an unsorted list (we used a dictionary example, which is technically a sorted list, just for a good analogy. But there are much better algorithms to find elements in a sorted list).</p>
</li>
<li><p>Printing all the elements in an array - you are going through all the elements of an array.</p>
</li>
<li><p>Counting the number of items in a list - again, you are going through all the elements of an array.</p>
</li>
</ul>
<h2 id="heading-ologn-logarithmic-time"><strong>O(logn) - Logarithmic Time</strong></h2>
<p>In our dictionary example, if you are opening in the middle and deciding to go left or right, then opening again in the middle and again deciding to go left or right and you keep on repeating it until you find the word. That is <strong>Binary Search Algorithm</strong>. It wouldn't work if the input elements are not sorted. This algorithm can give you O(<em>logn</em>).</p>
<p>"Logarithm" sounds scary, but it really isn't. Think of it this way: with O(<em>logn</em>) algorithms, you're not looking at every item. Instead, you're cutting the problem size in half (or some other fraction) with each step.</p>
<p>If you have a million pages in a dictionary:</p>
<p>First step: You cut it to 500,000 pages.</p>
<p>Second step: You cut it to 250,000 pages.</p>
<p>...and so on.</p>
<p>You can find your word incredibly fast because with each step, you eliminate a huge chunk of possibilities. This is why O(<em>logn</em>) is considered very efficient, especially for large inputs.</p>
<p><strong>Example:</strong></p>
<ul>
<li><p>Binary search - like our dictionary example, where the data must be sorted.</p>
</li>
<li><p>Finding an item in a balanced binary search tree.</p>
</li>
</ul>
<h3 id="heading-why-is-binary-search-ologn"><strong>Why is binary search O(logn)?</strong></h3>
<p>If n (your input size) is 100:</p>
<pre><code class="lang-python">After <span class="hljs-number">1</span> step, yo<span class="hljs-string">u're looking at 50 elements.
After 2 steps, you'</span>re looking at <span class="hljs-number">25</span> elements.
After <span class="hljs-number">3</span> steps, yo<span class="hljs-string">u're looking at 12-13 elements.
After 4 steps, you'</span>re looking at <span class="hljs-number">6</span><span class="hljs-number">-7</span> elements.
After <span class="hljs-number">5</span> steps, yo<span class="hljs-string">u're looking at 3-4 elements.
After 6 steps, you'</span>re looking at <span class="hljs-number">1</span><span class="hljs-number">-2</span> elements.
After <span class="hljs-number">7</span> steps, yo<span class="hljs-string">u're looking at 1 element (and you'</span>ll find it <span class="hljs-keyword">or</span> it<span class="hljs-string">'s not there).</span>
</code></pre>
<p>So, for 100 items, it takes about 7 steps.</p>
<p>Now consider n = 1,000,000 (one million).</p>
<pre><code class="lang-python"><span class="hljs-number">1</span>st step: <span class="hljs-number">500</span>,<span class="hljs-number">000</span>
<span class="hljs-number">2</span>nd step: <span class="hljs-number">250</span>,<span class="hljs-number">000</span>
<span class="hljs-number">3</span>rd step: <span class="hljs-number">125</span>,<span class="hljs-number">000</span>
...
<span class="hljs-number">10</span>th step: about <span class="hljs-number">1</span>,<span class="hljs-number">000</span>
...
<span class="hljs-number">20</span>th step: about <span class="hljs-number">1</span> element.
</code></pre>
<p>So, for a million items, it takes about 20 steps.</p>
<p>The number of steps is basically: how many times can you divide n by 2 until you're left with 1?</p>
<p>This "how many times do you divide by something" question is answered by a logarithm. Specifically, we're talking about the base-2 logarithm (log2).</p>
<pre><code class="lang-python">log2(<span class="hljs-number">100</span>)≈<span class="hljs-number">6.64</span> (which rounds up to <span class="hljs-number">7</span> steps)
log2(<span class="hljs-number">1</span>,<span class="hljs-number">000</span>,<span class="hljs-number">000</span>)≈<span class="hljs-number">19.93</span> (which rounds up to <span class="hljs-number">20</span> steps)
</code></pre>
<p>So, the number of operations grows proportionally to the logarithm (base 2) of the input size n. That's why we say its time complexity is O(log n).</p>
<h2 id="heading-on2-quadratic-time"><strong>O(n^2) - Quadratic Time</strong></h2>
<p>Imagine you're trying to find all possible pairs of students in a classroom. For every student, you have to pair them with every other student. If there are 'n' students, the first student pairs with (n-1) others, the second student pairs with (n-1) others, and so on. This quickly leads to n multiplied by n, or n^2 operations.</p>
<p>When you see nested loops in a computer program (a loop inside another loop), it often points to O(n^2) complexity. If you double the input size, the time taken doesn't just double; it quadruples! This can get very slow for large inputs.</p>
<p><strong>Example:</strong></p>
<ul>
<li><p>Bubble Sort (a simple but inefficient sorting algorithm where you repeatedly step through the list, compare adjacent elements and swap them if they are in the wrong order).</p>
</li>
<li><p>Finding all possible pairs of elements in a list.</p>
</li>
</ul>
<p>There are more like O(<em>n^3</em>) and O(<em>2^n</em>) and so on, which we are not covering in this article.</p>
<h2 id="heading-why-is-big-o-important-for-data-structures"><strong>Why is Big O important for Data Structures?</strong></h2>
<p>Data structures are ways of organizing data in a computer so that it can be used efficiently. The choice of data structure directly impacts the time complexity of the algorithms you use with it.</p>
<p>If you need to quickly check if an item exists, a hash table might give you an average O(1) search time. If you need to keep your data sorted and frequently search for items, a binary search tree might give you O(<em>logn</em>) search time. If you just need a simple list and don't care much about super-fast searching, a basic array might be fine for O(n) search time.</p>
<p>Understanding Big O helps you choose the algorithms to make your software perform well. It can be the difference between an app that runs in milliseconds and one that takes seconds… or minutes… or just crashes your computer.</p>
]]></content:encoded></item><item><title><![CDATA[Organizing Data in Structures]]></title><description><![CDATA[Let's try to understand the place where your computer remembers everything, its memory. As this plays a huge part in building efficient software.
Bit: The Smallest Piece of Information
Let's start with the absolute smallest element your computer can ...]]></description><link>https://blog.suryasathi.com/organizing-data-in-structures</link><guid isPermaLink="true">https://blog.suryasathi.com/organizing-data-in-structures</guid><category><![CDATA[data structures]]></category><dc:creator><![CDATA[Surya Sathi]]></dc:creator><pubDate>Fri, 18 Jul 2025 18:30:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1765777761598/59fdaebf-15b8-4e2a-82b1-8c2bc0bee865.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Let's try to understand the place where your computer remembers everything, its memory. As this plays a huge part in building efficient software.</p>
<h3 id="heading-bit-the-smallest-piece-of-information"><strong>Bit: The Smallest Piece of Information</strong></h3>
<p>Let's start with the absolute smallest element your computer can remember. It's called a bit. Think of a bit like a switch. It can be either on or off. That's it! Just two states. In the world of computers, "on" is usually represented by the number 1, and "off" by the number 0.</p>
<p>The idea of using just two states wasn't invented for computers. Back in 17th century, a German mathematician and philosopher named Gottfried Wilhelm Leibniz was fascinated by this binary system. He saw it as a beautiful reflection of creation – everything coming from nothing and one (like God). He believed it could simplify all of logic and reasoning.</p>
<blockquote>
<p>"Everything comes from nothing."</p>
</blockquote>
<h3 id="heading-byte-a-group-of-bits"><strong>Byte: A Group of Bits</strong></h3>
<p>Now if you want to store something meaningful, you can group these ons and offs together in different combinations. When 8 bits are grouped together, it's called a byte. Like 00000000, 00000001, 11111111 etc. So a byte can represent 256 different combinations of 0s and 1s and hence 256 different values.</p>
<p>For example letter 'A' can be represented by one combination, letter 'B' by another combination,  letter 'a' by some other combination and so on. This is how a computer stores everything. This is where you get the words KB, MB, GB, TB etc. from, when you store much larger files like notes, images, audio, video etc.</p>
<hr />
<h2 id="heading-data-structures-organizing-data"><strong>Data Structures: Organizing Data</strong></h2>
<p>If you think of the memory like a bookshelf filled with bytes, if you throw all the books around in some random order, it will be difficult to find anything, right? Same goes for computer memory. We have some specific ways of organizing data, that makes it easy to find, add or remove information. That is called <strong>data structures</strong>.</p>
<p>Think of data structures as different kinds of shelves or cabinets on our bookshelf, each designed for a specific purpose. Let's look at a few of them.</p>
<h3 id="heading-array-a-simple-list"><strong>Array: A Simple List</strong></h3>
<p>Imagine you have a series of identical boxes, all lined up neatly in a row. Each box can hold one piece of information. And you know exactly where each box is because they are numbered: Box 1, Box 2, Box 3, and so on.</p>
<p>This is very similar to an array. An array is like a continuous block of memory where you store similar types of data, one after the other. Each piece of data has a specific "address" or position (like the box number), which makes it super-fast to find any specific piece of data if you know its position.</p>
<pre><code class="lang-python">scores = [<span class="hljs-number">90</span>, <span class="hljs-number">85</span>, <span class="hljs-number">78</span>, <span class="hljs-number">92</span>]
print(scores[<span class="hljs-number">1</span>]) <span class="hljs-comment"># outputs 85</span>
</code></pre>
<p><strong>Note:</strong> Python lists behave like dynamic arrays, but allow mixed data types unlike typical arrays in C/C++/Java.</p>
<p><strong>Why is this useful?</strong></p>
<p>If you have a list of student scores, an array is perfect. You can quickly get the score of the 5th student, or the 100th student, because you just jump directly to their position.</p>
<hr />
<h3 id="heading-linked-list"><strong>Linked List</strong></h3>
<p>Now, imagine you have a bunch of individual boxes, but they are not lined up neatly. Instead, each box has a little reference inside it that tells you where the next box in the sequence is located. So, Box A tells you where Box B is, Box B tells you where Box C is, and so on.</p>
<p>This is like a linked list. Unlike an array, where items are stored right next to each other, a linked list stores data items in different, possibly scattered, locations in memory. But each item (called a "node") has two parts: the actual data, and a little piece of information called a pointer that "points" to the next item in the list.</p>
<p><strong>Pointer</strong></p>
<p>Think of a pointer as a tiny sticky note with an address written on it. This address tells your computer exactly where to find another piece of data in its memory. It's like saying, "Go to shelf number 5, slot 3, and you'll find the next part of this story." Pointers are how different pieces of data, even if they are physically far apart in memory, can be linked together logically.</p>
<pre><code class="lang-python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Node</span>:</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">init</span>(<span class="hljs-params">self, data</span>):</span>
        self.data = data
        self.next = <span class="hljs-literal">None</span>

<span class="hljs-comment"># Creating linked list: 10 -&gt; 20 -&gt; 30</span>

node1 = Node(<span class="hljs-number">10</span>)
node2 = Node(<span class="hljs-number">20</span>)
node3 = Node(<span class="hljs-number">30</span>)

node1.next = node2
node2.next = node3

current = node1
<span class="hljs-keyword">while</span> current:
    print(current.data)
    current = current.next

<span class="hljs-comment"># Outputs</span>
<span class="hljs-comment"># 10</span>
<span class="hljs-comment"># 20</span>
<span class="hljs-comment"># 30</span>

<span class="hljs-comment"># Inserting 40 between 20 and 30</span>

node4 = Node(<span class="hljs-number">40</span>)
node4 .next = node2.next
node2.next = node4

current = node1
<span class="hljs-keyword">while</span> current:
    print(current.data)
    current = current.next

<span class="hljs-comment"># Outputs</span>
<span class="hljs-comment"># 10</span>
<span class="hljs-comment"># 20</span>
<span class="hljs-comment"># 40</span>
<span class="hljs-comment"># 30</span>
</code></pre>
<p><strong>Why is this useful?</strong></p>
<p>Imagine you have a large collection of data. If you use an array, and you want to insert a new piece in the middle, you'd have to shift every single piece after that to make space. But with a linked list, you just create a new piece, change the "next piece" tag of the previous piece to point to your new piece, and then make your new piece point to the next one in the original sequence. It's much easier to add or remove elements in the middle without shuffling everything around.</p>
<hr />
<h3 id="heading-stack-lifo-abstraction-on-list"><strong>Stack: LIFO Abstraction on List</strong></h3>
<p>Imagine you're "stacking" plates. How do you usually stack them? You put one on top of the other, right? And when you want a plate, which one do you take first? The one on top. This "last in, first out" idea is exactly how a stack works.</p>
<p>Putting something on is called "pushing" onto the stack. You just add the new item right on top. Taking something off is called "popping" from the stack. You always remove the very last item you put on.</p>
<pre><code class="lang-plaintext">stack = []
stack.append("typed 'hello'")
stack.append("bolded text")
last_action = stack.pop()

print(last_action) # Outputs 'bolded text'
</code></pre>
<p><strong>Why is this useful?</strong></p>
<p>"Undo" action in a word processor. Each action you take (typing a letter, bolding text) is "pushed" onto an undo stack. When you hit undo, the last action is "popped" off and reversed.</p>
<hr />
<h3 id="heading-queue-fifo-abstraction-on-list"><strong>Queue: FIFO Abstraction on List</strong></h3>
<p>If you're at a store, and there's a "queue" of people waiting to pay. Who gets served first? The person who arrived first. And who's next? The person who joined the line right after them.</p>
<p>This "first in, first out" (FIFO) idea is how a queue works. You add a new item to the back of the queue (enqueueing). When you remove one, it's from the front (dequeuing).</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> collections <span class="hljs-keyword">import</span> deque

queue = deque()
queue.append(<span class="hljs-string">"Print job 1"</span>)
queue.append(<span class="hljs-string">"Print job 2"</span>)
next_job = queue.popleft()

print(next_job) <span class="hljs-comment"># Outpus 'Print job 1'</span>
</code></pre>
<p><strong>Why is this useful?</strong></p>
<p>Queues are essential for managing tasks where order matters. When you browse the internet, your computer sends requests to websites. These requests might get put into a queue to be processed in order.</p>
<hr />
<h3 id="heading-tuple"><strong>Tuple</strong></h3>
<p>This is an ordered collection, just like a list, where you can look at them, you can count them, but you can't change them, add new ones, or remove old ones from that specific list.</p>
<pre><code class="lang-python">coordinates = (<span class="hljs-number">10</span>, <span class="hljs-number">20</span>)
print(coordinates[<span class="hljs-number">0</span>]) <span class="hljs-comment"># outputs 10</span>
</code></pre>
<p><strong>Why is this useful?</strong></p>
<p>Tuples aren’t just immutable lists. They’re often used when heterogeneity and data grouping are needed (e.g., (x, y) coordinates, function returns).</p>
<hr />
<h3 id="heading-dictionary"><strong>Dictionary</strong></h3>
<p>Think about a set of index cards. On each card, you write a keyword (like "Apple"). And then, on the same card, you write its definition (like "a round fruit"). If you want to find the definition of "Apple," you quickly look for the "Apple" card.</p>
<p>This "keyword and definition" idea is what we call a key-value pair.</p>
<p>Key: This is the unique identifier, like the keyword on the index card. It's what you use to look up the information.</p>
<p>Value: This is the actual data or information associated with that key, like the definition on the index card.</p>
<p>So, a dictionary is a collection of these key-value pairs.</p>
<pre><code class="lang-python">person = {<span class="hljs-string">"name"</span>: <span class="hljs-string">"Alice"</span>, <span class="hljs-string">"age"</span>: <span class="hljs-number">30</span>}
print(person[<span class="hljs-string">"age"</span>]) <span class="hljs-comment"># Outputs 30</span>
</code></pre>
<p><strong>Why is this useful?</strong></p>
<p>When you look up a customer by their ID, or a product by its unique code, a hash map is very likely working behind the scenes.</p>
<p>When you define variables in a program (e.g., age = 30), the programming language often uses a hash map to store the variable name (age is the key) and its value (30 is the value).</p>
<p>Imagine a website that serves millions of users. Instead of going to the main database every time someone asks for a popular piece of information, the website might store that information in a "cache" using a hash map. The "key" is the request, and the "value" is the answer, allowing for lightning-fast delivery.</p>
<hr />
<h3 id="heading-tree"><strong>Tree</strong></h3>
<p>A tree data structure is a way of organizing data in a hierarchical fashion (like a family tree).</p>
<p>The very top item is called the root (like the main ancestor or the tree's base). Each item can have "children" (items below it, connected by a line), and these children can also have their own children, and so on. Items at the very bottom, with no children, are called leaves (like the leaves on a real tree).</p>
<pre><code class="lang-python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TreeNode</span>:</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">init</span>(<span class="hljs-params">self, data</span>):</span>
        self.data = data
        self.children = []

root = TreeNode(<span class="hljs-string">"C:"</span>)
docs = TreeNode(<span class="hljs-string">"Documents"</span>)
down = TreeNode(<span class="hljs-string">"Downloads"</span>)
pics = TreeNode(<span class="hljs-string">"Pictures"</span>)
movs = TreeNode(<span class="hljs-string">"Movies"</span>)

root.children = [docs, down]
docs.children = [pics]
down.children = [movs]

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">print_children</span>(<span class="hljs-params">prefix, node</span>):</span>
    print(prefix, node.data)
    <span class="hljs-keyword">for</span> child <span class="hljs-keyword">in</span> node.children:
        print_children(prefix + <span class="hljs-string">'---'</span>, child)

print_children(<span class="hljs-string">''</span>, root)

<span class="hljs-comment"># Outputs</span>
<span class="hljs-comment">#  C:</span>
<span class="hljs-comment"># --- Documents</span>
<span class="hljs-comment"># ------ Pictures</span>
<span class="hljs-comment"># --- Downloads</span>
<span class="hljs-comment"># ------ Movies</span>
</code></pre>
<p>There are different types of trees like Binary Trees, Binary Search Trees etc.</p>
<p><strong>Why is this useful?</strong> Trees are incredibly powerful for organizing data that has a natural hierarchy.</p>
<p>File Systems is a perfect example! Your main drive (C:) is the root, then you have folders like "Documents," "Pictures," "Programs," etc., which are its children. Each of those folders can contain more folders or files, extending the branches of the tree.</p>
<hr />
<h3 id="heading-graph"><strong>Graph</strong></h3>
<p>Imagine a map with many cities. Some cities are connected by roads, others are not. A road might go both ways, or just one way. And some roads might be longer or shorter (representing distance or time).</p>
<p>That's a graph! It's a collection of "nodes" (which are like our cities) and "edges" (which are like the roads connecting the cities). The edges can be one-way or two-way, and they can even have "weights" (like the distance of a road).</p>
<p>There are different types like directed/undirected, weighted/unweighted, cyclic/acyclic, sparse/dense graphs.</p>
<pre><code class="lang-python">graph = {
    <span class="hljs-string">"A"</span>: [<span class="hljs-string">"B"</span>, <span class="hljs-string">"C"</span>],
    <span class="hljs-string">"B"</span>: [<span class="hljs-string">"C"</span>],
    <span class="hljs-string">"C"</span>: [<span class="hljs-string">"A"</span>],
    <span class="hljs-string">"D"</span>: [<span class="hljs-string">"C"</span>]
}

<span class="hljs-comment"># This is a direct unweighted graph that says there is an edge from A to B, A to C and C to A but not B to A. Similarly there is an edge from D to C but not from C to D.</span>

graph = {
    <span class="hljs-string">"HYD"</span>: [(<span class="hljs-string">"BLR"</span>, <span class="hljs-number">480</span>), (<span class="hljs-string">"CHN"</span>, <span class="hljs-number">600</span>)],
    <span class="hljs-string">"BLR"</span>: [(<span class="hljs-string">"HYD"</span>, <span class="hljs-number">480</span>), (<span class="hljs-string">"CHN"</span>, <span class="hljs-number">360</span>), (<span class="hljs-string">"KOC"</span>, <span class="hljs-number">540</span>)],
    <span class="hljs-string">"CHN"</span>: [(<span class="hljs-string">"HYD"</span>, <span class="hljs-number">600</span>), (<span class="hljs-string">"BLR"</span>, <span class="hljs-number">360</span>)],
    <span class="hljs-string">"KOC"</span>: [(<span class="hljs-string">"BLR"</span>, <span class="hljs-number">540</span>)]
}

<span class="hljs-comment"># Now, this is an undirected weighted graph.</span>
</code></pre>
<p><strong>Why is this useful?</strong></p>
<p>Graphs are for representing complex relationships like:</p>
<p>Social Networks: Think of Facebook or Instagram. Each person is a "node," and if two people are friends, there's an "edge" connecting them. You can use graphs to find out who's friends with whom, or to recommend new friends.</p>
<p>Maps: When you use Google Maps to find the shortest route between two places, it's using graph algorithms. Each intersection or landmark is a node, and the roads are edges, with their lengths as weights.</p>
<p>The Internet itself: Websites are nodes, and the links between them are edges! This is how search engines like Google "crawl" the web.</p>
<hr />
<h2 id="heading-closing"><strong>Closing</strong></h2>
<p>Understanding these different ways of organizing data is a huge step in truly understanding how computers work efficiently.</p>
<p>If you need to quickly add and remove items from the end (like undoing actions), a stack is great. But if you need to process tasks in the order they arrive (like print jobs), a queue is the way to go.</p>
<p>If you need to store hierarchical data (like files on your computer), a tree is ideal. But if you need to represent complex relationships (like friends on a social network), a graph is perfect.</p>
<p>It's not just about storing information; it's about storing it in a way that makes it easy and fast to find, add, delete, and process, which is at the heart of building powerful and responsive software.</p>
<blockquote>
<p>"The art of memory is the art of attention." – Samuel Johnson</p>
</blockquote>
]]></content:encoded></item><item><title><![CDATA[Why Are There So Many Programming Languages? - Part 2]]></title><description><![CDATA["So, if C was such a powerful language, why didn't it remain the sole dominant language ruling every domain, every program, and every software ever created? Why were Python, JavaScript, Java, and Go and a hundred other languages created?"
This was th...]]></description><link>https://blog.suryasathi.com/why-are-there-so-many-programming-languages-part-2</link><guid isPermaLink="true">https://blog.suryasathi.com/why-are-there-so-many-programming-languages-part-2</guid><category><![CDATA[programming languages]]></category><category><![CDATA[Why]]></category><category><![CDATA[understanding]]></category><category><![CDATA[history]]></category><dc:creator><![CDATA[Surya Sathi]]></dc:creator><pubDate>Sun, 06 Jul 2025 18:30:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1765777197422/825a2d2e-de82-4987-b5c7-c25121421485.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>"So, if C was such a powerful language, why didn't it remain the sole dominant language ruling every domain, every program, and every software ever created? Why were Python, JavaScript, Java, and Go and a hundred other languages created?"</em></p>
<p>This was the question which we ended the previous part with. To give you an analogy, it's like asking why we have bicycles, bikes, cars, ships and airplanes when horses could get us around. Each serves a different purpose.</p>
<p>The world kept expanding and trying to solve some of the problems with C is painfully long and inefficient. And this is where the story of <strong>specialization</strong> begins. Think of it like this, early humans did everything themselves - farming, building shelters, hunting and cooking. But as civilizations evolved, we got different people taking up different occupations, specializing in one thing - we got farmers, builders, doctors, chefs etc. Programming languages, sort of, followed the same path.</p>
<h3 id="heading-rise-of-object-oriented-thinking"><strong>Rise of Object-Oriented Thinking</strong></h3>
<p>One of the biggest shift after C was OOP (Object-Oriented Programming). It is about writing code based on how we see the world. If you are to write the properties of a dog in C, you will do something like, dog_color, dog_weight, dog_name and some functions like walk_dog, feed_dog etc. But in OOP, we get all of this together by creating objects, which contain both properties and functions bundled.</p>
<blockquote>
<p><em>"The whole is greater than the sum of its parts." -</em> Aristotle</p>
</blockquote>
<p>So in OOP, you'd define a Dog blueprint (which is called a <strong>Class</strong>) which says "Every dog has a name, age and a color. It can eat and walk and sleep.". Then, whenever you need to create a dog in your code, you will reference this class to create a dog object with that particular dog's properties. Every dog will have its own name, age and color. It's like having self-contained units.</p>
<p>This concept makes code much easier to understand, maintain and reuse in large programs. That said, it shouldn't always be the go to. Think before you apply. Sometimes simple functions can do :-)</p>
<h2 id="heading-c-c-with-classes"><strong>C++: "C with Classes"</strong></h2>
<p>The first big step in that direction was C++ which was also created at Bell Labs, just like C. He initially called it C with Classes, because that's what it is.</p>
<p>C was good for system-level programming – things close to the hardware, like operating systems. But as software grew more complex, especially for large applications, managing all the interactions with just C became difficult. C++ allowed programmers to keep C's efficiency and control over hardware while making organization easier with OOP.</p>
<p>C++ became the go-to for complex applications, game development, and high-performance computing where speed mattered, but organization was also crucial.</p>
<p>While C++ was making big applications more manageable, a whole new area was opening up: the World Wide Web. Suddenly, people wanted programs that could run on different types of computers, over networks, and easily interact with information spread across the globe.</p>
<h2 id="heading-java-write-once-run-anywhere"><strong>Java: "Write Once, Run Anywhere"</strong></h2>
<p>Created in the mid-1990s, it had a revolutionary idea at its core, "Write Once, Run Anywhere".</p>
<p>Remember how C programs needed to be compiled specifically for the type of computer it's running on? If you wrote a C program on Windows, it wouldn't run on Mac or Linux without recompiling it.</p>
<p>Java solved this by introducing something called a Java Virtual Machine (JVM) between the code and the system. It is like a universal translator mini-computer running on your actual computer. When you write Java code, it's compiled into an intermediate form called bytecode. This bytecode isn't tied to any specific type of computer. Instead, the JVM on any computer will take the trouble to understand and run this bytecode on the particular type of computer it is running on.</p>
<p>Java quickly became immensely popular for enterprise applications and large-scale systems because of its portability, security features and its strong support for OOP.</p>
<h2 id="heading-javascript-interactive-webpages"><strong>JavaScript: "Interactive Webpages"</strong></h2>
<p>Around the same time as Java, another language was born with the <strong>specific purpose of making webpages interactive</strong>. Before this, the webpages were plain HTML and CSS. You can view them and they were colorful as well but not interactive. Just text and images. Couldn't click on a button and have something happen in the page. JavaScript changed that by running on browsers and allowing dynamic content like menus and forms bringing life to websites.</p>
<p>Without JavaScript, the web as we know it today – with its rich, interactive experiences like social media, maps and streaming services – simply wouldn't exist.</p>
<p><strong>Fun Fact:</strong> A common misconception is that Java and JavaScript are related because of the "Java" in the name. But they are two completely different languages made for different purposes. It was just a marketing gimmick by Netscape, the company that created JavaScript, to capitalize on the growing popularity of Java at the time.</p>
<p>Ober a decade later Node JS was also born with the intention of running JS on the servers, not just browsers, powering backend as well. So with Node JS, JavaScript became a full-stack language, meaning one can use same language for both frontend and backend applications.</p>
<p>This is an advantage, <strong>when you need faster development</strong> as you don't need a developer who knows different languages and one doesn't need to switch between two different languages when working on frontend and backend.</p>
<h2 id="heading-python-code-should-be-readable"><strong>Python: "Code should be Readable"</strong></h2>
<p>Python, created in the late 1980s (though I am mentioning it after Java, it was created even before that), initially didn't have the explosive growth as Java. But it gained immense popularity later, due to its philosophy that code should be readable first and developers should spend more time on actual logic with fewer lines than C and CPP.</p>
<p>Python also transforms the code to bytecode behind the scenes and runs it on Python Virtual Machine (PVM), but it doesn't involve an explicit compilation step like Java, and does the compilation on the fly, leading to its tag of "interpreted language". Meaning, if you write a code with Java, due to its static typing and explicit compilation step, you will be able to catch many issues before runtime.</p>
<p>But still, Python is much more easier to learn, and it has a vast ecosystem with countless pre-built libraries for data science, AI, web development etc. Due to its simple syntax and the libraries available, you can easily prototype and test your ideas.</p>
<p>Python became the language of choice for data scientists, AI researchers and basically anyone who needed to get things done quickly without sacrificing much performance.</p>
<h2 id="heading-go-minimalism-and-concurrency"><strong>Go: "Minimalism and Concurrency"</strong></h2>
<p>Google faced a massive problem: their systems were incredibly complex, highly distributed, and needed to handle millions of requests concurrently (at the same time). Existing languages often made this difficult or error-prone. So, Go, often called Golang, was created at Google in 2009.</p>
<p>Its philosophy is it be simple, efficient and has concurrency as a core part of its design with "Goroutines". While other languages do support concurrency with the concept of "threads", Goroutines are generally much more lighter (consume less CPU and memory) and faster.</p>
<p>It is very fast in compilation, its built-in concurrency features make it ideal for building things like web servers and microservices (small, independent parts of a large application) and it avoids many complex features found in other languages (like inheritance from traditional OOP), making it easier to maintain by introducing new ways to handle the same things.</p>
<h2 id="heading-so-why-so-many"><strong>So, Why So Many?</strong></h2>
<p>Just like one wouldn't go to a dentist for a stomach problem, different problems require different specializations with different tools. C++ is great for operation systems, JS is great for webpages, Python is great for data science and Go is great for highly concurrent services. Each language comes with a set of rules and philosophy, some prioritize speed, some readability and others safety. Just like a vibrant community and libraries made Python a de facto standard language for data science, the ecosystem plays a role in which language you choose for your application.</p>
<p>As the world evolves, needs change and new languages will keep emerging.</p>
<p>You don't need to learn every language you heard of. Knowing, why a language exists, what problem it's trying to solve, its strengths and weaknesses, helps you choose the right tool for the job.</p>
<blockquote>
<p><em>"For every tool there is a task, and a tool for every task."</em></p>
</blockquote>
]]></content:encoded></item><item><title><![CDATA[Why Are There So Many Programming Languages? - Part 1]]></title><description><![CDATA[Abhay just joined his bachelor's program, and he started learning to code during his first year of college. Being a complete beginner, he thought:

"Once I learn a programming language, I'm good to go."

He began with Python because his friends said ...]]></description><link>https://blog.suryasathi.com/why-are-there-so-many-programming-languages-part-1</link><guid isPermaLink="true">https://blog.suryasathi.com/why-are-there-so-many-programming-languages-part-1</guid><category><![CDATA[history]]></category><category><![CDATA[Why]]></category><category><![CDATA[programming languages]]></category><category><![CDATA[understanding]]></category><dc:creator><![CDATA[Surya Sathi]]></dc:creator><pubDate>Thu, 19 Jun 2025 18:30:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1765776372285/03ffb5b1-cfe1-407b-94a8-6aef4321dc47.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Abhay just joined his bachelor's program, and he started learning to code during his first year of college. Being a complete beginner, he thought:</p>
<blockquote>
<p>"Once I learn a programming language, I'm good to go."</p>
</blockquote>
<p>He began with Python because his friends said that it is clean, readable, and easy. Then came C in his college course. It felt raw and mechanical, with memory pointers and manual management. Later, he had to pick up JavaScript, for a side project. His classmates were learning Java, Go, and Rust, and he was feeling pressured to learn them as well.</p>
<p>That's when he paused and thought:</p>
<blockquote>
<p>"Why are there so many programming languages? And why do people keep inventing new ones instead of just improving the old ones?"</p>
</blockquote>
<p>Now, to make a list, we have C, C++, Python, Java, JavaScript, R, Rust, Go, Ruby, Kotlin, .NET, etc. The names above are just the ones I have heard of and can recall at the moment. There are many, many more. And people needed to learn multiple languages to build proper applications.</p>
<h3 id="heading-now-the-question"><strong>Now the question</strong></h3>
<p>Why..? Why make life hard with so many of them?</p>
<p>To understand this better, try to think of the theory of evolution, not the biological kind in full, but an evolution guided by humans. Life evolved from single-celled organisms to multicellular organisms living in water, some stepped onto land, evolved into multiple types of organisms, each having its own features to fit and survive in the environment it's living in. Among them are primates, which evolved into apes, a particular species then evolved into humans.</p>
<p>So, once humans were here, did all the other apes disappear? Or did the fish just vanish because some organism stepped onto land? Of course not. Each species adapted to its niche.</p>
<p>Though programming is a <strong>human-driven evolution</strong>, similar to the biological one, one language didn't replace the other. One after another, solved <strong>different</strong> problems - scientific, business, systems, web and so on. Some got obsolete - yes, similar to the species that couldn't adapt. But those that stuck around have accumulated and now we have a variety of languages existing together.</p>
<h2 id="heading-the-evolution-of-computer-language"><strong>The Evolution of Computer Language</strong></h2>
<h3 id="heading-where-it-all-started"><strong>Where it all started</strong></h3>
<p>Now, before stepping into programming languages, let's start with where it all started: Computation. Alan Turing introduced a concept called the Turing Machine, a mathematical model that could simulate the logic of any algorithm. The idea is to prove that any computable problem can be solved by a general purpose machine, what we now think of as separating what needs to be done (a software in today's terms) from the machine executing it (the bare metal, hardware). This is one of the most important contributions to the computer world as nearly all modern programming languages are Turing-complete.</p>
<h3 id="heading-painful-but-still-a-proof-of-concept"><strong>Painful but still a proof of concept</strong></h3>
<p>Turing helped design a machine, Bombe <em>(it was not based on the Turing Machine model though)</em>, during World War II, to decipher German codes. You can say it's the first attempt to automate logic-based tasks. Though he had a tragic death, he did a great service to humanity with his inventions. Around the same time, <strong>ENIAC</strong> was born. One had to rewire physical cables and flip switches to program it. Here, programs weren't lines of code but hardware configurations.</p>
<h3 id="heading-architecture-for-programming"><strong>Architecture for programming</strong></h3>
<p>Then came the <strong>Von Neumann Architecture</strong>, which introduced the idea of storing both data and instructions in memory. Here, the term "program" takes on a better meaning. One can load different instructions into memory without rewiring the machine like ENIAC. You just need to change the bits in the memory.</p>
<p>BUT, <strong>you need to write the program in pure binary, 1s and 0s.</strong> Very painful and error-prone, it led to the creation of <strong>assembly languages</strong>, giving symbolic, human-readable names to instructions and memory locations. It is still extremely low-level but better than writing binary. But there was a different assembly for every processor.</p>
<h3 id="heading-lets-make-it-readable"><strong>Let's make it readable</strong></h3>
<p>But the idea took hold: why shouldn't humans write human-readable terms and let the machine do the work of translating it to 1s and 0s? That is the idea behind a compiler.</p>
<h3 id="heading-making-it-easy-for-scientists"><strong>Making it easy for scientists</strong></h3>
<p>Initially, IBM created <strong>FORTRAN</strong> (FORmula TRANslation - 1957), as the name suggests, to translate algebraic formulas into machine code to help scientists write math-heavy programs without needing to know assembly language. This made it possible to write expressions like <em>X = A + B \</em> C*; then the compiler will turn it into machine code and make them work.</p>
<h3 id="heading-making-it-easy-for-businesses"><strong>Making it easy for businesses</strong></h3>
<p>Then there was <strong>COBOL</strong> (COmmon Business Oriented Language - 1959), which was more business-focused. COBOL emphasized English-like syntax with syntax like <em>IF HOURS &gt; 40 THEN COMPUTE OVERTIME-PAY = HOURS \</em> OTPAY*, so non-programmers can read it better. It is said to have dominated enterprise applications and to still be found in some legacy systems.</p>
<h3 id="heading-laying-the-foundation"><strong>Laying the foundation</strong></h3>
<p>Before COBOL, there was <strong>Lisp</strong> (1958), using lists (catch it in the name). It was the first time implement garbage collection, meaning a language can automatically manage memory without you needing free it when you no longer need it, popularized recursion, and the idea of functional programming. It is said to be the foundation of early AI programming.</p>
<p>There was also <strong>ALGOL</strong> (ALGOrithmic Language - 1960), which isn't as widely used commercially, but introduced language design with the idea of scopes, block structure with a beginning and an end, and a syntax that influenced nearly all future languages. It was the first to separate <strong><em>syntax</em></strong>, how the program looks, from <strong><em>semantics</em></strong>, what it does. This leads to the development of C.</p>
<p>Next came <strong>BASIC</strong> (Beginner's All-purpose Symbolic Instruction Code - 1964), programming for everyone. It was meant for education, simple enough to teach students. It exploded on home computers and helped democratize programming.</p>
<h3 id="heading-software-crisis"><strong>Software Crisis</strong></h3>
<p>Then came the period known as the Software Crisis. Computers were evolving rapidly, but software wasn't. Codebases became messy and hard to maintain everywhere, a phenomenon later became known as '<strong>spaghetti code</strong>'; there wasn't a structured discipline which led to a need for structured programming with type safety and better compilers.</p>
<p>Around this time, at Bell Labs, Ken Thompson needed a language to rewrite some parts of the UNIX OS. So he created B from BCPL. It was typeless and minimalistic. Dennis Ritchie, working with Thompson, extended B into C, adding types, structure, better memory control, etc., for systems development. They used it to rewrite the UNIX kernel.</p>
<h3 id="heading-why-c"><strong>Why C?</strong></h3>
<p><strong>C</strong> (1972) succeeded because it hit the sweet spot. It talks <strong>directly to hardware</strong>, similar to assembly language, so it is efficient and powerful. But <strong>abstract and readable enough</strong> to manage larger programs with variables and data structures, algebraic expressions, control flow (conditional statements and iterations), reuse with function calls, and <strong>memory management</strong> with pointers, etc.</p>
<p>Also, it was highly <strong>portable</strong> compared to its predecessors; you can write it once and compile it on other machines (though with minor changes). It follows <strong>structured programming</strong> with scopes and other concepts from ALGOL.</p>
<blockquote>
<p>C is one of the oldest programming languages that you can still find widely being taught. While many people recommend that beginners start their programming journey with Python, as it is much more readable and very high-level, I personally think C is better for starting, as I believe it is the most grounded one, without any wrappers, which helps you understand what programming is and how logic works at a lower level. It can be brutal though.</p>
</blockquote>
<h2 id="heading-whats-next"><strong>What's Next?</strong></h2>
<p>So, if C was such a powerful language, why didn't it remain the sole dominant language ruling every domain, every program, and every software ever created? Why were Python, JavaScript, Java, and Go and a hundred other languages created?</p>
<p>To answer that, we need to look at why programming languages began not just evolving, but diverging, a phenomenon you must have observed by now in this article, even after the rise of C. And that's what we will explore in "<strong>Why Are There So Many Programming Languages? - Part 2.</strong>" We will go over how different programming languages emerged one after the other, over time, each designed to solve specific challenges, reflect unique philosophies, or serve a particular domain or community, ultimately understanding why we need a multitude of programming languages.</p>
]]></content:encoded></item></channel></rss>