<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Signal Over Noise</title>
        <link>https://en-signal.ceak.dev</link>
        <description>Ideas, systems and the stories behind technology.</description>
        <language>en</language>
        <copyright>&#169; 2026 Signal Over Noise</copyright>
        <atom:link href="https://en-signal.ceak.dev/rss/" rel="self" type="application/rss+xml" />
        <lastBuildDate>Thu, 04 Jun 2026 07:34:32 +0900</lastBuildDate>

                <item>
                    <title>Incidents That Brought the Internet to a Halt — Moments When a Tiny Piece of Code Shook Global Infrastructure</title>
                    <link>https://en-signal.ceak.dev/00080300-software-history-season3-internet-breaking-incidents/</link>
                    <guid isPermaLink="true">https://en-signal.ceak.dev/00080300-software-history-season3-internet-breaking-incidents/</guid>
                    <pubDate>Sat, 25 Apr 2026 21:54:16 +0900</pubDate>
                    <dc:creator><![CDATA[ceak]]></dc:creator>
                    <category><![CDATA[⚙ Essays]]></category><category><![CDATA[📚 Defining Moments in Software History]]></category><category><![CDATA[📚 Incidents That Brought the Internet to a Halt]]></category><category><![CDATA[Software History]]></category><category><![CDATA[Open Source]]></category><category><![CDATA[DevOps]]></category>
                    <description><![CDATA[The internet seems like a unified system, but it is built on countless small pieces of code and dependencies. This article shows how tiny components—like left-pad, Log4Shell, and Heartbleed—can disrupt global infrastructure and reveals the true nature of the software ecosystem we rely on.
]]></description>
                    <content:encoded><![CDATA[<h2 id="incidents-that-brought-the-internet-to-a-halt-%E2%80%94-moments-when-a-tiny-piece-of-code-shook-global-infrastructure">Incidents That Brought the Internet to a Halt — Moments When a Tiny Piece of Code Shook Global Infrastructure</h2><p>We often understand the internet as a massive system. A complex structure where countless servers, data centers, and global networks are interconnected. That is not wrong. However, when you look a little deeper into this enormous system, a completely different picture emerges. The internet is not a single massive system, but a vast structure composed of numerous small pieces that depend on and connect to each other. And many of those pieces are surprisingly small, simple, and sometimes nothing more than code maintained by one or two developers.</p><p>This reality does not usually reveal itself. When everything operates normally, that complexity of dependencies and fragility remains completely hidden, appearing as a stable system. But one day, when a very small crack appears, the situation unfolds in a completely different way. That crack spreads far faster than expected, and eventually, services we took for granted begin to fail in a chain reaction. The stories covered in this series are records of exactly those moments. They are not merely simple bugs or accidents, but events that expose how delicately balanced the system called the internet truly is.</p><h2 id="a-world-built-on-invisible-connections">A World Built on Invisible Connections</h2><p>Modern software no longer exists as a single program. Most services operate by depending on dozens or even hundreds of libraries and open-source projects. Developers do not implement everything themselves. Instead, they build systems by combining and connecting code that already exists. This approach has dramatically increased development speed and, at the same time, has explosively expanded the software ecosystem.</p><p>However, this structure inherently carries one assumption: <strong>that someone else’s code, somewhere, will always exist and function properly</strong>. We import npm packages, use open-source libraries, and accept countless dependencies without question. And in most cases, that assumption holds true without issue. But the moment that assumption breaks, the systems we have built begin to shake far more easily than expected.</p><p>The npm left-pad incident is the simplest yet most powerful example of this structure. A mere 11 lines of code were removed, yet countless projects failed to build simultaneously. It was not just an accident, but an event that revealed how deeply we depend on each other’s code. This incident exposed an uncomfortable truth to developers: <strong>we are building our world on code we do not directly control</strong>.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-131.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-131.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-131.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-131.png 1536w" sizes="(min-width: 720px) 720px"></figure><h2 id="vulnerabilities-do-not-start-in-code-but-in-structure">Vulnerabilities Do Not Start in Code, but in Structure</h2><p>If the left-pad incident exposed the problem of “dependency,” Log4Shell and Heartbleed revealed something even deeper. These were not simply errors in code, but structural vulnerabilities. We often understand security vulnerabilities as bugs in specific pieces of code. But in reality, the bigger issue lies in where that code is positioned and how far its influence extends.</p><p>Log4j was just a logging library. Most developers are not even consciously aware of it. It simply exists as a component automatically used within frameworks or libraries. But that was precisely the problem. Because too many systems depended on the same library at a deeply embedded level, once a vulnerability was discovered, its impact spread uncontrollably. The day when companies around the world had to apply patches simultaneously was a moment that revealed how extensively the software supply chain is interconnected.</p><p>Heartbleed carries a similar context. OpenSSL was a core library responsible for internet security, yet the number of maintainers was extremely limited. Countless companies and services depended on it, but the structure maintaining that code was highly fragile. This incident raises an important question: <strong>are we truly managing the core infrastructure used by the entire world properly?</strong></p><h2 id="attacks-no-longer-target-systems-but-the-supply-chain">Attacks No Longer Target Systems, but the Supply Chain</h2><p>As time passes, the problem becomes more complex. It goes beyond simple bugs or vulnerabilities, and the attacks themselves begin to directly target the structure of software. The SolarWinds incident represents that turning point. The attacker did not directly attack a specific system. Instead, they infiltrated the software update process and spread malicious code through a legitimate distribution channel. This approach completely neutralized existing security models.</p><p>After this incident, the concept of security changed significantly. It is no longer sufficient to ask, “Is our system secure?” Now, we must ask, <strong>“Is all the code and every tool we use secure?”</strong> And this is not an easy question to answer. That is because modern software depends on too many external components.</p><p>The XcodeGhost incident follows the same pattern. By tampering with official development tools to distribute malicious code, the attack turned the very environment that developers trusted into a target. This incident demonstrates that even the development environment is no longer a safe zone. We now live in a world where everything we use—tools, libraries, packages, and even the update process—can become targets of attack.</p><h2 id="understanding-the-structure-of-the-world-we-built">Understanding the Structure of the World We Built</h2><p>The incidents covered in this series are not merely records of past accidents. They are critical clues for understanding the structure of the software world we use today. The common pattern revealed through these events is clear. Software is no longer an isolated system, but a vast interconnected structure, and that very connectivity becomes the source of risk.</p><p>As developers, we make countless choices in pursuit of productivity and efficiency. We use libraries, install packages, and combine tools that have already been built. These choices are rational, and in most cases, they are the right ones. However, the result created by the accumulation of those choices is a structure far more complex than we expect. And that structure has a way of coming back to us in ways we did not anticipate.</p><p>This series is about understanding that structure. It traces how a single small piece of code could shake the world, why such events continue to happen repeatedly, and what we should learn from them. The internet is not merely a collection of technologies, but the result of countless decisions, dependencies, and collaborations. The moment we understand that, the systems we use every day begin to look entirely different.</p><p>And only then do we begin to ask:</p><p><strong>Do we truly understand this massive system?</strong></p>]]></content:encoded>
                </item>
                <item>
                    <title>Tools That Changed Developer Culture — How the Way We Work Was Shaped</title>
                    <link>https://en-signal.ceak.dev/00080299-software-history-developer-tools-epilogue/</link>
                    <guid isPermaLink="true">https://en-signal.ceak.dev/00080299-software-history-developer-tools-epilogue/</guid>
                    <pubDate>Mon, 20 Apr 2026 17:28:59 +0900</pubDate>
                    <dc:creator><![CDATA[ceak]]></dc:creator>
                    <category><![CDATA[⚙ Essays]]></category><category><![CDATA[📚 Defining Moments in Software History]]></category><category><![CDATA[📚 Tools That Changed Developer Culture]]></category><category><![CDATA[Software History]]></category><category><![CDATA[Developer Tools]]></category><category><![CDATA[Developer Culture]]></category>
                    <description><![CDATA[The way we develop software today did not emerge naturally. Tools like Git, GitHub, Stack Overflow, npm, and Docker arose to solve specific problems, and together their choices formed modern developer culture.]]></description>
                    <content:encoded><![CDATA[<h2 id="where-the-things-we-take-for-granted-began">Where the Things We Take for Granted Began</h2><p>We write code every day, create commits, install packages, and deploy applications. All of these processes feel so familiar that they no longer seem remarkable. Like air, or electricity, they have become a foundation we use naturally without conscious awareness. But this sense of normalcy did not exist from the beginning. Rather, the way we develop software today is the result of countless events, decisions, failures, and coincidences layered over time.</p><p>The history of software is not merely a history of technological advancement. It is a record of the problems people faced and how they attempted to solve them. Sometimes a personal hobby project reshaped global infrastructure. Sometimes a strategic decision by a company created an entirely new ecosystem. And in some cases, a mere eleven lines of code brought the entire internet to a halt. Through this series, we have followed those moments—not focusing simply on <strong>what was created</strong>, but on <strong>why it was needed</strong>, and <strong>what changed as a result</strong>.</p><h2 id="a-history-of-choices-not-just-technology">A History of Choices, Not Just Technology</h2><p>If there is a single thread running through this series, it is that the evolution of software has been driven less by technology itself and more by a series of choices. Linux was not just an operating system; it was the result of choosing community-driven collaboration over centralized development. Git, likewise, was not simply a tool with new features, but a response to the limitations of existing collaboration methods. Stack Overflow introduced a new way of sharing knowledge, and GitHub transformed open source into a platform.</p><p>These changes did not begin with grand declarations of innovation. Most started as practical decisions to solve immediate problems. But as those decisions accumulated, they eventually reshaped the entire culture of development. The workflow we use today was built from these layers of choices. Creating branches, submitting pull requests, installing packages, and deploying with containers are not just technical procedures—they are the outcomes of decisions made in the past.</p><p>What matters here is that the way we work today is <strong>not the only possible answer</strong>. It is simply one form that emerged from what was, at the time, the most reasonable set of choices. And that also means it can change again.</p><h2 id="small-tools-massive-change">Small Tools, Massive Change</h2><p>Many of the events we explored in this series seemed trivial at first. Git was just a version control system. npm was merely a package manager for JavaScript libraries. Docker began as a tool to simplify how applications are run. Yet these tools went far beyond convenience; they fundamentally restructured how development itself works.</p><p>This transformation happens gradually. At first, a tool appears to offer simple convenience. Over time, it becomes a prerequisite. Today, it is almost unimaginable to develop software without a package manager. Likewise, deploying applications without containers is increasingly seen as inefficient. In this way, tools embed themselves deeper and deeper into the development process until they eventually <strong>redefine the very assumptions of development itself</strong>.</p><p>What is particularly interesting is that these changes are not always positive. Incidents like npm’s left-pad and Log4Shell reveal how fragile our systems can be. Just as a small tool can drive massive change, a small vulnerability can ripple across the entire world. These events expose the reality that the systems we have built are both incredibly powerful and inherently fragile.</p><h2 id="a-connected-world-and-invisible-dependencies">A Connected World and Invisible Dependencies</h2><p>Modern software no longer exists in isolation. A single application depends on dozens, sometimes hundreds, of libraries, and those libraries depend on others in turn. This structure enables rapid development, but it also creates layers of complexity that we rarely perceive.</p><p>The events covered in this series clearly illustrate this interconnectedness. GitHub connected developers into a global network. npm expanded code reuse to an extreme degree. Docker and Kubernetes separated applications from infrastructure, enabling consistent execution anywhere. Together, these changes transformed software into a vast, interconnected system.</p><p>But connectivity has two sides. As connections increase, efficiency improves—but so does risk. When a single package disappears or a vulnerability is discovered, the impact can spread far faster and wider than expected. We have seen this through the events in this series. These were not merely accidents, but reflections of the fundamental nature of the systems we have built.</p><h2 id="where-the-next-change-will-begin">Where the Next Change Will Begin</h2><p>Even now, new tools and platforms are emerging. In particular, AI-driven development tools are beginning to redefine how software is built. Code generation, automated testing, and even design-level assistance are no longer experimental—they are becoming part of everyday development. This shift is not just about improving productivity; it is about transforming the role of the developer itself.</p><p>Looking back, every major shift began with a specific problem. Git emerged because collaboration was difficult. Stack Overflow was created because knowledge sharing was inefficient. So what problems are we facing today? And what tools will emerge to solve them?</p><p>It is possible that the next decisive moment has already begun. We may simply not yet recognize its scale. Historically, change has always started gradually, then crossed a threshold where it expanded rapidly. After that point, returning to previous ways of working became impossible.</p><h2 id="what-choices-are-we-standing-on">What Choices Are We Standing On</h2><p>What this series ultimately reveals is a clear truth: the world of software is not just a collection of technologies, but the result of choices. Decisions about what tools to build, what approaches to adopt, and what structures to maintain have shaped the ecosystem we have today.</p><p>And those choices were made by people. Linus Torvalds created Git. Countless developers contributed to open source projects. Communities shared knowledge through Stack Overflow and collaborated through GitHub. Everything we use today began as someone’s attempt to solve a problem.</p><p>This leads to an important question. What choices will we make going forward? Whether we adopt new tools, maintain existing approaches, or attempt entirely new directions will shape the future of development in ways we cannot yet fully predict.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-42.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-42.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-42.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-42.png 1536w" sizes="(min-width: 720px) 720px"></figure><h2 id="in-the-end-it-is-not-just-about-code">In the End, It Is Not Just About Code</h2><p>We often think of software as code. But what this series reveals is that something even more important exists beyond the code itself. It is the way code is created, shared, and executed. In other words, software is not just a product—it is an entire process.</p><p>Linux, Git, GitHub, Stack Overflow, npm, Docker, and countless tools that followed have all reshaped that process. And this transformation is far from over. The moment we assume that today’s methods are permanent, a new tool may emerge to challenge that assumption.</p><p>Ultimately, the history of software is not the story of finished systems, but of an ongoing evolution. And we are still standing in the middle of it.</p>]]></content:encoded>
                </item>
                <item>
                    <title>Readium Devlog #6 — From “No Backend” to a Boundary Layer</title>
                    <link>https://en-signal.ceak.dev/readium-devlog-6-backend-boundary-layer/</link>
                    <guid isPermaLink="true">https://en-signal.ceak.dev/readium-devlog-6-backend-boundary-layer/</guid>
                    <pubDate>Sat, 18 Apr 2026 16:00:03 +0900</pubDate>
                    <dc:creator><![CDATA[ceak]]></dc:creator>
                    <category><![CDATA[📱 Apps]]></category><category><![CDATA[📚 Readium Development Log — Why Design Keeps Failing]]></category><category><![CDATA[Backend Architecture]]></category><category><![CDATA[System Design]]></category><category><![CDATA[Mobile Development]]></category>
                    <description><![CDATA[Avoiding a backend felt like the right decision at first. But external APIs, security, and system boundaries eventually broke that assumption. This post explores how a “no-backend” design evolved into a minimal boundary layer between the client and the outside world.]]></description>
                    <content:encoded><![CDATA[<h2 id="why-the-choice-to-not-build-a-server-felt-natural">Why the Choice to Not Build a Server Felt Natural</h2><p>When first designing Readium, the initial assumption was simple. Avoid building a server if possible. This decision was not so much a technical challenge as it was an attempt to eliminate unnecessary complexity. When looking at the problem of reading records, most of the data belongs to the individual user. What books were read, when they were read, and how far they were read are not pieces of information that need to be shared externally. From this perspective, a server does not appear as something that expands functionality, but rather as something that increases the burden of management. The moment authentication, data storage, synchronization, backup, and security come into play, the scope of the problem expands rapidly. So at the beginning, the intention was to deliberately exclude all of these concerns. The starting point of the design itself was “do not build a server.”</p><p>This judgment was not based on pure idealism, but on a structure that was actually implementable. Mobile devices had already become sufficiently powerful, and by using a local database, most states could be managed entirely within the device. In particular, a SQLite-based structure was already a proven choice in terms of stability and performance. An application that works without a network clearly offers advantages in terms of user experience. Even when the connection is lost, records remain, input is not blocked, and the app continues to behave consistently. These characteristics align well with the act of reading. Reading happens anytime and anywhere, and an experience that does not depend on network conditions feels more natural. As all these conditions aligned, the conclusion that “removing the server is the right decision” gradually solidified into certainty.</p><p>The problem was not that this judgment was wrong, but that it felt too natural to be questioned. There was a moment when it shifted from “not building a server” to “believing that a server is unnecessary.” This difference may seem small, but it becomes decisive at the point where the design begins to collapse. At the time, this choice felt like a more refined design. It removed unnecessary layers, clarified data ownership, and kept the system simple. However, this simplicity only holds under certain conditions. The moment those conditions are not clearly defined, and only the conclusion “a server is not needed” remains, the design is already vulnerable.</p><p>At this point, what matters is not what was built, but what was excluded. Choosing to remove the server is not just about reducing components, but about redefining the scope of problems the system must solve. But that redefinition was incomplete. Some problems disappeared, but others had simply not yet appeared. And those problems would soon surface.</p><h2 id="what-the-local-first-architecture-actually-solved">What the Local-First Architecture Actually Solved</h2><p>Instead of removing the server, the chosen structure was a local-first architecture. This structure is not merely about storing data locally, but about shifting the center of the system into the device itself. In Readium, all core data structures exist within the local database. Reading state, session records, and timeline events are all created and stored within the device. The network is only an optional element, not a required component. This structure is simple yet powerful. The app maintains a consistent state at all times, and its functionality is not constrained by external environments.</p><p>The effectiveness of this structure was repeatedly confirmed during implementation. Starting and ending a reading session can be fully handled without any network requests. Even if the user closes the app while reading, the state is restored exactly as it was when reopened. The timeline is also constructed purely from local events, without synchronization with a server. All of these flows are reflected immediately without delay, resulting in a much more natural experience for the user. Since there is no need to wait for network requests, interactions are never interrupted. At this stage, the local-first strategy did not just seem like a choice, but like the correct answer.</p><p>In particular, managing both state and events locally carries significant architectural implications. In a serverless structure, there is no need for complex synchronization logic to maintain data consistency. There is no need to resolve conflicts, nor to design retry strategies for network failures. The system becomes much simpler, and the scope of problems is reduced. This simplicity accelerates development and reduces maintenance overhead. As a result, this structure continued to reinforce confidence. The belief that “this app can be fully completed without a server” naturally took shape.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-136.png" class="kg-image" alt="" loading="lazy" width="1024" height="559" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-136.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-136.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-136.png 1024w" sizes="(min-width: 720px) 720px"></figure><p>However, the problem begins the moment the boundary between what this structure solves and what it does not solve is not clearly distinguished. Local-first provides an answer to “where should data be stored,” but it does not answer “where should data come from.” This distinction is not apparent at first. As long as the app operates only on data it already has, there is no issue. But the moment the user tries to input new data, the limitations of this structure begin to emerge.</p><p>At this stage, the design had not yet collapsed. It even appeared close to completion. But that sense of completeness was built on certain conditions. The problem was that those conditions had not been clearly defined. And those conditions would be broken by a very simple user action.</p><h2 id="the-first-crack-the-problem-of-%E2%80%9Cchoosing-a-book%E2%80%9D">The First Crack: The Problem of “Choosing a Book”</h2><p>In order to record a reading activity, there is a prerequisite step that must occur. The user must choose which book they are reading. This step is so obvious that it is not initially recognized as part of the design. The user enters a book title into a search field and selects one from the list. A cover image is displayed, along with the author and publisher information. This experience has been repeated across countless services, so it is accepted without much thought. The problem is that this familiar experience cannot be created using only local data.</p><p>When the app is first launched, there is no book data available. In order to display results based on the user’s input, the corresponding data must exist somewhere. But the local database does not contain this information. If the user has previously read the book, some data might remain, but the moment a new book is searched, this structure offers no help. It is at this point that the first gap in the design becomes visible. The local-first structure is powerful when dealing with already existing data, but it provides no answer for creating or retrieving data that does not yet exist.</p><p>This problem cannot be solved simply by adding a single feature. Book metadata contains a vast amount of information and is continuously updated. Collecting and managing data for millions of books is beyond the scope of an individual project. In other words, this is not a question of “how to implement it,” but “who has this data.” And the answer is clear. It already exists externally.</p><p>At this moment, the focus of the design shifts. Until now, the central question had been “how to record.” Now it becomes “what data should be used to start the recording.” This change may seem small, but it alters the boundary of the system. A structure that was previously closed within internal data now transitions into one that depends on external data. This transition is the first crack.</p><p>This crack does not immediately collapse the system. Instead, it naturally leads to the next decision. Use an external API. By leveraging an already existing database, the problem can be easily solved. This judgment is not wrong. But the fact that this choice introduces another problem only becomes visible in the next stage.</p><h2 id="the-moment-of-confronting-the-reality-of-external-book-apis">The Moment of Confronting the Reality of External Book APIs</h2><p>After recognizing the problem of book metadata, the available choices were simpler than expected. This was because leveraging already existing data was the most realistic approach. Services such as Kakao Books API or Google Books API have already built massive book databases, and with a simple search request, they return information such as cover images, authors, publishers, and ISBNs. These APIs are not merely convenience features, but are in fact closer to “the only viable solution for services that do not own data.” A structure where an individual collects, refines, and continuously updates such data is not sustainable in terms of maintenance cost. Therefore, using external APIs was not a choice, but a decision that was almost inevitable.</p><p>At this stage, the design still does not appear to be significantly shaken. The local-first structure remains intact, and the external API is simply added as a “tool for fetching data.” The mobile app can directly call the API, receive the response, and display it on the screen. This approach is simple to implement and does not significantly conflict with the existing structure. The app continues to operate centered around local data, and the external API only assists with initial input. At this point, the overall structure even appears more complete. It feels as though a missing piece of the puzzle has been filled.</p><p>However, there is an important assumption hidden within this decision. It assumes that a structure relying on external APIs is safe and sustainable. On the surface, this assumption does not seem problematic. Most sample code and tutorials also demonstrate calling APIs directly from mobile applications. In the early stages of development, this approach is the fastest and most intuitive. In practice, it is also not difficult to implement. You send an HTTP request and parse the JSON response. This simplicity actually obscures the problem. Despite the existence of structural risks, the ease of implementation hides those risks from view.</p><p>At this point, the design has not yet collapsed. It continues to flow naturally. Placing an external API on top of a local-first structure is intuitive and accelerates development. However, this structure accepts one assumption without any verification. That assumption is the question: “Is it acceptable for the client to directly call external APIs?” This question soon rises to the center of the problem.</p><h2 id="the-problem-with-calling-apis-directly-from-mobile">The Problem with Calling APIs Directly from Mobile</h2><p>A structure where a mobile app directly calls an external API appears to have no issues at first. You obtain an API key, include it in the request header, and the data is returned correctly. However, this structure inherently contains a vulnerability. The API key is embedded in the client. This key may be just a simple string, but it represents access rights to the entire service. And this string is easier to expose than expected. Extracting the key is not difficult through methods such as analyzing the APK file or capturing network traffic.</p><p>This problem does not end at the level of a simple security vulnerability. The exposure of the API key means that the origin of requests can no longer be controlled. If someone obtains this key, they can call the API directly without going through the app. The number of requests may increase, and once usage limits are reached, the API provider may block the key. In severe cases, the service itself may be disrupted. In other words, this structure is one that will inevitably break at some point. It is not merely a possible issue, but something closer to a matter of time.</p><p>More importantly, this problem is not one that can be resolved through technical measures alone. Code obfuscation or network encryption does not provide a fundamental solution. Any information embedded in the client will eventually be exposed externally. In this structure, the API key is an “asset that cannot be protected.” And building a system that depends on an unprotectable asset is a flawed design choice. At this point, it can no longer be viewed as an issue of implementation. The conclusion is reached that the structure itself is wrong.</p><p>The moment this problem is recognized, the previously smooth flow of design comes to a halt. The approach of layering an external API on top of a local-first structure can no longer be maintained. Data must be fetched from external sources, but the path cannot be the client. This contradiction cannot be resolved through simple modifications. It reaches a point where the design itself must be redefined.</p><h2 id="the-collapse-of-the-design-it-was-not-about-removing-the-server-but-redefining-it">The Collapse of the Design: It Was Not About Removing the Server, but Redefining It</h2><p>At the beginning, the goal was to avoid building a server. However, this goal could no longer be maintained when confronted with the reality of external APIs. The issue was not that a server had to be added, but rather “how the role of the server should be defined.” Simply introducing a traditional backend system was an excessive choice. A conventional server that stores user data and processes business logic does not fit this structure. The core data of Readium must still remain local, and the server must not intrude upon that structure.</p><p>At this point, the design changes direction. Instead of removing the server, it is redefined with a strictly limited role. The server does not hold state. It does not have a database. It does not store user information. Instead, it performs only one role. It calls external APIs on behalf of the client and securely delivers the results. This role may appear small, but structurally it carries significant meaning. It creates a boundary between the client and the external world.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-137.png" class="kg-image" alt="" loading="lazy" width="1024" height="559" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-137.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-137.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-137.png 1024w" sizes="(min-width: 720px) 720px"></figure><p>This server is not just a simple proxy. It goes beyond relaying requests and defines the boundary of the system. The API key no longer exists in the client, but is managed only within the server. The client does not directly depend on specific external APIs, but uses only the interface provided by the server. This structure does more than just strengthen security. It reduces the coupling of the entire system. Even if external APIs change, the client remains unaffected.</p><p>This change is not merely an adjustment at the level of “adding a server.” The center of the design has shifted. If the initial goal was to remove the server, the focus now becomes how to define boundaries. The server is no longer the center of the system, but a minimal mechanism for maintaining boundaries. At the moment this transition occurs, the local-first strategy is redefined into a more stable form. It becomes clear that the true goal was not to eliminate the server, but to control the scope of its influence.</p><h2 id="backend-as-a-protective-layer">Backend as a Protective Layer</h2><p>After redefining the role of the server in the direction of minimizing it, the structure begins to take shape more clearly. The mobile app no longer calls external Book APIs directly. Instead, it calls the Readium Backend, which has a single fixed endpoint. The server receives this request, internally calls the external APIs, processes the result, and then returns it to the client. On the surface, this may look like simple mediation, but in reality, this is where the separation of responsibilities within the system occurs. The client no longer needs to be aware of the existence of external APIs, and the server absorbs any changes in those external APIs internally.</p><p>This structure does more than simply strengthen security. By breaking the direct connection between the client and external APIs, the coupling of the system is significantly reduced. For example, even if the response structure of a particular API changes or the service is discontinued, the client code does not need to be modified. Adjustments can be made solely within the server. Additionally, this structure naturally provides a foundation for expansion into combining multiple APIs. Even if only one API is used initially, it can later evolve into a system that sequentially calls multiple APIs or merges results from different sources. All of these changes are handled transparently from the client’s perspective.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-138.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-138.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-138.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-138.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>At the code level, this structure can be simplified into the following flow:</p><pre><code>Client → Readium Backend → External APIs</code></pre><p>This representation is simple, but it carries important architectural meaning. A constraint is introduced where all external requests must pass through the Backend, and this constraint itself creates system stability. The API key exists only within the server, and the client has no knowledge of it. Since the form of requests is controlled by the server, the usage pattern of external APIs can also remain consistent. Ultimately, this Backend is not just a relay layer, but becomes a protective layer that simultaneously handles security and abstraction.</p><p>At this point, the server is no longer something that “should be removed.” Instead, it becomes a structural component that is necessary to maintain system stability. However, its role is still limited. It does not store data and does not hold state. It performs only the role of maintaining boundaries. Because of this limitation, the server does not grow or become complex. And this characteristic continues to influence subsequent decisions.</p><h2 id="why-fastify-instead-of-express">Why Fastify Instead of Express</h2><p>As the role of the server becomes clear, the choice of technology stack also naturally narrows. This server does not process complex domain logic. It has no database and does not manage state. It simply receives requests, calls external APIs, and returns the results. This simplicity changes the criteria for choosing a framework. In a typical backend system, ecosystem size or extensibility may be important, but here, lightness and a predictable structure are more critical.</p><p>In the Node.js environment, the most widely used framework is still Express. It has long been considered a de facto standard, with abundant resources and examples available. However, Express does not enforce structure due to its flexibility. In smaller projects, this flexibility can actually harm consistency. If the shape of requests and responses is not clearly defined, the code gradually becomes loose and difficult to maintain. The Readium Backend is small in scale, but its structure needed to be clear.</p><p>At this point, Fastify appeared to be a more suitable choice. Fastify allows defining requests and responses based on schemas, and validation and serialization are automatically handled based on those schemas. This is not just a convenience feature, but a mechanism that firmly defines the server’s interface. Because the contract between the client and server is expressed at the code level, it becomes easy to identify the impact scope when changes occur. Additionally, its internal JSON serialization performance is very fast, reducing unnecessary overhead even in simple request handling.</p><p>Its integration with TypeScript was also an important factor. When schemas are defined, request and response types are automatically inferred, eliminating the need to repeatedly define types separately. Even for a small server, type safety remains important. In fact, the simpler the structure, the more easily the entire flow can collapse when types break. Fastify naturally compensates for this. Ultimately, this choice was not about “wanting to use a new technology,” but about selecting the tool that best fits the current structure.</p><p>This decision also influences how the server evolves in the future. Because the structure is clearly defined, even when new features are added, the existing interface can be easily maintained. And this consistency directly contributes to the overall stability of the system. In the end, choosing a framework was not merely a matter of technical preference, but a decision that extended from the design itself.</p><h2 id="a-choice-to-not-operate-servers-cloud-run">A Choice to Not Operate Servers: Cloud Run</h2><p>After constructing the server, the remaining question was “how to operate this server.” The moment a server exists, a new responsibility called operations arises. Managing a VM directly requires handling operating system updates, security patches, log management, and scaling. All of these tasks are separate from application development, yet they continuously demand time in practice. In a personal project, this burden accumulates quickly. When the cost of maintaining the server exceeds that of developing features, the project naturally comes to a halt.</p><p>To avoid this problem, Cloud Run was chosen. Cloud Run is a serverless platform that operates based on container images. Developers only need to create and upload a Docker image, and the platform automatically handles execution and scaling. The core of this structure is request-based execution. Containers run only when requests come in, and when there are no requests, no resources are used. In other words, the server exists, yet at the same time, it can remain in a state of non-existence.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-139.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-139.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-139.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-139.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>This approach aligns well with the characteristics of the Readium Backend. When traffic is low, costs are almost nonexistent, and resources are used only when needed. Additionally, since there is no need for direct server management, developers can focus solely on application code. This is not just a matter of convenience, but is directly tied to the sustainability of the project. The less operational burden there is, the more time can be spent on feature development and structural improvements.</p><p>This choice is also a design decision. It is a decision to have a server, but not to operate it. A physical server exists, but it is excluded from the domain of management. This approach does not conflict with the local-first strategy. The server still performs only a minimal role, and the infrastructure is also kept minimal. In the end, Cloud Run is not just a deployment environment, but another structural choice made to minimize the presence of the server itself.</p><h2 id="authentication-as-the-second-boundary-firebase-authentication">Authentication as the Second Boundary: Firebase Authentication</h2><p>Even after introducing the server, the structure was not yet complete. While the issue of protecting external API keys had been resolved, another question naturally followed. Who is allowed to use this server? If the API is exposed without any restrictions, this server would no longer function as a protective layer but would instead become a public proxy that calls external APIs on behalf of anyone. If someone discovers this endpoint and begins sending arbitrary requests, the server would end up handling unintended traffic. Eventually, this leads back to the same outcome as before. API usage increases, limits are reached, and the entire service becomes affected.</p><p>At this point, the server must go beyond simply “forwarding requests” and take on the role of determining the origin of those requests. In other words, the server must now handle two boundaries simultaneously. One is the boundary that protects external APIs, and the other is the boundary that protects the server itself. The solution chosen to establish this second boundary was Firebase Authentication. The mobile app performs user authentication through Firebase, and once authentication is complete, it receives an ID Token. This token is not just a simple string, but signed data that can identify a specific user and session.</p><p>When the server receives a request, it also receives this token and processes the request only after verifying it. This process goes beyond simply checking whether the user is logged in. It becomes the basis for determining whether the request originated from the actual app and whether it was generated by a valid user session. Through this structure, the server is no longer an endpoint open to everyone, but an interface that can only be accessed within a restricted scope.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-140.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-140.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-140.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-140.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>This decision carries structural significance. The server evolves from a simple protective layer into a gateway that simultaneously handles security and request control. Protecting external APIs alone was not sufficient; the server itself also needed protection in order for the entire system to remain stable. With this second boundary in place, the Backend finally takes shape as an independent system.</p><h2 id="why-this-server-is-small-but-important">Why This Server Is Small but Important</h2><p>The Readium Backend constructed in this way is very small in scale. It has no database and no complex domain logic. The types of requests it handles are also limited. However, when viewed structurally, the importance of this server exists on an entirely different level than its size might suggest. This server is not the center of the system, but rather defines the boundaries of the system. And these boundaries determine the stability of the entire structure.</p><p>The client no longer connects directly to external APIs. All requests are routed through the server, which validates and processes them. In this process, security, request control, and API abstraction are all handled simultaneously. Even if external APIs change, the client remains unaffected, and unauthorized requests are blocked at the server. Additionally, combining or replacing multiple APIs can be handled entirely within the server. All of these changes remain transparent to the client.</p><p>From a system-wide perspective, this server functions more like a gateway than a traditional backend. The data still resides locally, and the server does not own any data. Instead, it controls the connection points with the outside world and manages all incoming requests into the system. This role is not immediately visible, but structurally, it is one of the most critical components.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-141.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-141.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-141.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-141.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>Ultimately, this server is defined not by its size, but by its role. It is not simple because it is small; it remains small because its role is deliberately constrained. And this constraint is the key factor that stabilizes the structure. At this point, it becomes clear that it is more important to define the server in such a way that it does not need to grow, rather than trying to prevent it from growing after the fact.</p><h2 id="redefining-the-design-it-is-not-about-eliminating-the-server-but-reducing-the-boundary">Redefining the Design: It Is Not About Eliminating the Server, but Reducing the Boundary</h2><p>The initial goal when designing Readium was clear. Avoid building a server as much as possible. This goal was partially maintained, but it was not realized exactly as intended. Instead, it was redefined into a completely different form. Rather than eliminating the server, the structure shifted toward limiting the server’s role as much as possible. This change was not a compromise, but a natural result of a deeper understanding of the design.</p><p>Attempting to completely eliminate the server may seem ideal, but in real systems, a boundary with the external world is always necessary. Data can remain internal, but not all inputs and outputs can stay within the system. The moment external data is involved, the more important question becomes how to define that boundary. In Readium, the server remained as the minimal mechanism responsible for that boundary. It was possible to add more roles to the server, but this was intentionally avoided.</p><p>This decision ultimately strengthens the local-first strategy. What matters is not the absence of a server, but that the server does not intrude upon core data. Data ownership remains with the client, and the server only handles issues that arise externally. This structure is both simple and extensible. If necessary, the server’s role can gradually expand, but the fundamental boundary remains intact.</p><p>At this point, the discussion returns to the original question. Is a server necessary? The answer is no longer simple. A server may or may not be necessary. What matters is not its existence, but the role it plays. The Readium Backend represents one answer to that question. It is not about removing the server, but about redefining what the server means.</p><p>And on top of this structure, another problem emerges. Is a system that depends on a single API truly stable? How should failures in search be handled, and how should different data sources be combined? These questions still remain. In the next article, we will explore in more detail the Provider and Fallback strategies introduced to address these issues.</p><h2 id="next-step-is-one-api-enough">Next Step: Is One API Enough?</h2><p>Based on the structure so far, the Readium Backend appears to have reached a sufficiently stable form. External API keys are securely protected within the server, and the client no longer directly depends on external services. Through authentication, the origin of requests is controlled, and the server maintains the boundaries of the entire system while performing only a minimal role. Looking at this state alone, the design feels as though it has been fully organized. In practice, when using a single Book API, the system operates without major issues. A search request returns results, and the user can select one of them to begin recording their reading activity.</p><p>However, this structure still maintains one underlying assumption. It assumes that a specific external API will always return correct results. At first, this assumption is rarely questioned. Most searches work normally, and for common book titles, sufficiently accurate results are provided. But the situation changes as soon as the user attempts slightly different forms of input. When a book title is entered only partially, contains typos, or when data is insufficient depending on language or region, search results may be empty or inaccurate. This is not merely an edge case, but a factor that continuously disrupts the actual user experience.</p><p>At this point, another question emerges. Is a structure that depends on a single API truly stable? External APIs are systems beyond our control. Response formats may change, services may temporarily go down, or certain data may be missing. Up to now, the dependency has been “hidden” through the server, but the dependency itself has not been reduced. In other words, the structure has become safer, but it is still tied to a single point of failure.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-142.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-142.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-142.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-142.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>This problem cannot be solved simply by adding another API. The moment multiple APIs are introduced, a new design is required. Decisions must be made about the order in which APIs are called, how results are combined, and what strategy should be used when failures occur. In other words, the concept of a “search strategy,” which was not considered in a single API structure, now emerges. This strategy is not just an implementation detail, but is directly connected to user experience. The quality of the application depends on which results are shown first and how failures are handled.</p><p>Ultimately, at this point, the structure expands once again. The server is no longer just a layer that forwards requests, but also takes on the role of selecting and combining multiple data sources. However, this expansion does not occur in the same way as before. Instead of arbitrarily increasing the server’s responsibilities, it proceeds by layering a new strategy on top of the existing concept of a <strong>protective layer</strong>.</p><p>The next article will address this exact point. It will explore how multiple Book APIs can be integrated into a single search experience, and how to design a structure that assumes failure as a given. Through the abstraction of Providers and the introduction of a Fallback strategy, we will examine in detail the process of moving away from dependency on a single API.</p>]]></content:encoded>
                </item>
                <item>
                    <title>The Day Docker Arrived: The Beginning of the DevOps Revolution</title>
                    <link>https://en-signal.ceak.dev/00080205-docker-day-devops-cloud-native-revolution/</link>
                    <guid isPermaLink="true">https://en-signal.ceak.dev/00080205-docker-day-devops-cloud-native-revolution/</guid>
                    <pubDate>Fri, 17 Apr 2026 17:21:36 +0900</pubDate>
                    <dc:creator><![CDATA[ceak]]></dc:creator>
                    <category><![CDATA[⚙ Essays]]></category><category><![CDATA[📚 Defining Moments in Software History]]></category><category><![CDATA[📚 Tools That Changed Developer Culture]]></category><category><![CDATA[Docker]]></category><category><![CDATA[DevOps]]></category><category><![CDATA[Cloud Native]]></category>
                    <description><![CDATA[Docker was more than a container tool. It solved structural deployment limitations and fundamentally changed how development, operations, and infrastructure are managed, marking the beginning of the DevOps and Cloud Native era.]]></description>
                    <content:encoded><![CDATA[<h2 id="the-world-before-docker-%E2%80%94-why-deployments-always-broke">The World Before Docker — Why Deployments Always Broke</h2><p>One of the most strangely recurring phrases in software development was this: “But it works on my machine.” This sentence was not just a joke, but the most accurate expression of the structural problems embedded in development environments at the time. Developers would write code and run it successfully in their local environment without issues. However, the moment the same code was deployed to a server, unexpected errors would occur. This problem was not simply due to configuration mistakes, but rather stemmed from the fundamental separation between development and production environments. Differences in operating system versions, installed libraries, environment variables, and even subtle variations in file system structures could produce entirely different outcomes.</p><p>This problem could not simply be dismissed as a few developers making mistakes. In fact, as software systems became more complex, the issue appeared more frequently. As web applications increasingly relied on diverse libraries and frameworks, the number of conditions required to run a single application grew exponentially. Developers were forced to spend significant time not only writing code but also aligning the environment necessary for that code to run. This process was often documented in lengthy installation guides spanning dozens of pages, and within teams, the success of deployment often depended on how precisely these documents were followed.</p><p>The problem did not end there. Even when developers followed the same documentation, it was common for identical environments not to be reproduced. Minor differences in library versions, operating system patch levels, or even the order of dependency installation could lead to different results. This lack of reproducibility created persistent uncertainty for developers. Eventually, deployments began to fail more often due to environmental issues rather than code problems, becoming one of the primary factors slowing down development speed.</p><p>During this period, developers spent more time fighting the environment than writing code. Troubleshooting involved analyzing logs, comparing server and local environments, and aligning library versions repeatedly. What mattered most in this process was not purely technical knowledge, but experience and intuition—knowing which library might cause issues and which configuration might affect the system. In other words, deployment was less a systematic process and more an area dependent on the instincts of experienced developers. And it was precisely at this point that software development began to hit a fundamental limitation.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-36.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-36.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-36.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-36.png 1536w" sizes="(min-width: 720px) 720px"></figure><h2 id="the-limitations-of-existing-solutions-%E2%80%94-why-virtual-machines-were-not-the-answer">The Limitations of Existing Solutions — Why Virtual Machines Were Not the Answer</h2><p>The first attempt to solve this problem was to <strong>replicate the environment itself</strong>. In other words, the idea was to recreate exactly the same environment as the production server on the developer’s local machine. This approach naturally led to virtualization technology. Tools like VMware and VirtualBox allowed multiple virtual machines to run on a single physical machine, each with its own independent operating system. Developers could now create a local environment identical to the production server and test their applications within it.</p><p>This approach certainly addressed many of the earlier issues. It reduced the differences between development and production environments and allowed applications dependent on specific operating systems or libraries to run more reliably. However, this solution introduced a new set of problems. Virtual machines were inherently heavy. Running a single application required booting an entire operating system along with all necessary components, resulting in images that were several gigabytes in size and required significant time to start. Managing multiple environments simultaneously demanded substantial system resources.</p><p>Furthermore, virtual machines were not well-suited for rapidly evolving development environments. Each time an application changed, developers often needed to rebuild or reconfigure the VM environment, which slowed down the development process. As microservices architecture began to emerge, where systems were split into multiple independently deployable services, VM-based approaches became increasingly inefficient. Managing dozens of virtual machines quickly became a significant operational burden.</p><p>In the end, virtualization solved one problem while exposing another limitation. Replicating the entire environment ensured stability but lacked flexibility and speed. Developers still needed a solution that was lightweight, fast, and capable of guaranteeing identical environments. At this point, a fundamental question resurfaced: was it truly necessary to replicate an entire operating system to achieve environmental consistency?</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-37.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-37.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-37.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-37.png 1536w" sizes="(min-width: 720px) 720px"></figure><h2 id="the-idea-of-containers-%E2%80%94-a-technology-that-already-existed-but-was-hard-to-use">The Idea of Containers — A Technology That Already Existed but Was Hard to Use</h2><p>The answer to this question had already existed. There was a way to isolate processes without replicating an entire operating system. This capability came from features provided by the Linux kernel. Technologies such as cgroups and namespaces made it possible to run processes in isolated environments, making them appear as if they were running on independent systems. This concept is what we now call containers.</p><p>Containers, unlike virtual machines, share the underlying operating system. This makes them significantly lighter and faster to execute. Since they only include the application and its required libraries, container images are small and can start almost instantly. In theory, this structure provided an ideal solution for ensuring identical environments between development and production. However, the challenge was that this technology was too complex for developers to use directly.</p><p>Early container technologies such as LXC existed, but leveraging them required a deep understanding of Linux systems. The setup process was complicated, and defining environments was not intuitive. In other words, the technology was there, but the tooling was not. Developers still had to manually configure environments, and in many cases, using containers was actually more difficult than traditional approaches.</p><p>At this point, what mattered was not the technology itself, but how easily it could be used. Containers were already a powerful concept, but they had not yet been transformed into something accessible to everyone. As a result, developers were still forced to choose between virtual machines and manual configuration, with no complete solution in sight.</p><p>And it was precisely at this moment that a turning point emerged. A tool appeared that restructured existing technology in a completely new way, making it practical and usable for developers. That tool was Docker.</p><h2 id="the-emergence-of-docker-%E2%80%94-what-was-different">The Emergence of Docker — What Was Different</h2><p>As discussed earlier, the concept of containers already existed before Docker. The problem was that this technology did not function as a practical solution for developers. Handling containers directly required deep knowledge of the Linux kernel, and configuring environments remained complex and inconvenient. Ultimately, developers were stuck in a gap between “possible technology” and “usable tools,” still carrying the burden of unresolved problems. It was precisely at this point that Docker appeared.</p><p>Docker’s core innovation was not the creation of new technology, but the <strong>reconfiguration</strong> of existing technology in a fundamentally different way. The concept of the Dockerfile illustrates this transformation most clearly. Developers no longer needed to write lengthy documents describing how to set up an environment. Instead, they could define the execution environment with just a few lines of code. By specifying the base image, required libraries, and execution commands, Docker could automatically reproduce the exact same environment. At this moment, the environment was no longer dependent on human memory or documentation, but became a reproducible and fixed state.</p><p>Docker also introduced the concept of images as a <strong>portable unit of environment</strong>. Once created, an image behaves identically regardless of where it is executed. An application that runs on a developer’s local machine can run exactly the same on a server or in a cloud environment. The most important shift here was the disappearance of the idea of “matching environments” and the emergence of the idea of “bringing the environment with you.” This distinction was not merely about convenience—it marked a turning point that fundamentally changed the paradigm of software deployment.</p><p>Another key feature of Docker was its layered image structure. A Docker image is not a single monolithic entity but is composed of multiple layers. This structure allows systems to share a common base while adding or modifying only what is necessary. As a result, build times become faster and storage is used more efficiently. In other words, Docker did not simply standardize execution environments; it also provided a way to manage them efficiently.</p><p>In this way, Docker abstracted away technical complexity and allowed developers to focus on solving problems. By transforming the concept of containers into a <strong>usable tool</strong>, Docker set the stage for a deeper shift in how software is developed. This shift extended beyond tools and began to redefine the entire process of building and deploying software.</p><h2 id="%E2%80%9Cenvironment-as-code%E2%80%9D-%E2%80%94-how-docker-changed-development-practices">“Environment as Code” — How Docker Changed Development Practices</h2><p>The most significant impact of Docker was not just technical efficiency, but the way it changed how developers perceive environments. Previously, environments were something that had to be manually configured and maintained. Developers followed installation guides, adjusted settings step by step, and investigated logs when issues arose. Docker completely inverted this process. Instead of manually configuring environments, developers could now define them in code and generate them automatically.</p><p>This shift had a profound impact on the entire development workflow. Developers could now version-control not only their application code but also the execution environment. It became possible to reproduce the exact environment of any point in time, which brought enormous advantages to debugging and testing. For example, reproducing a bug from the past no longer required rebuilding the environment from scratch. Running the corresponding Docker image would instantly recreate the exact same conditions.</p><p>This structure became even more powerful when combined with CI/CD pipelines. When code changes were made, Docker images could be automatically built, tested, and deployed. In this process, issues caused by environment inconsistencies were nearly eliminated. Development, testing, and production environments all ran on the same image, ensuring consistency across the entire pipeline. As a result, deployment transformed from a risky event into a repeatable and stable process.</p><p>Ultimately, Docker introduced a crucial concept: <strong>“the environment is code.”</strong> This idea goes beyond technical convenience and influences how software systems are designed. As environments became codified, the boundary between infrastructure and application began to blur. Developers were no longer just writing code; they were also responsible for designing the environments in which that code runs. This transformation laid the foundation for what would soon be recognized as DevOps culture.</p><h2 id="the-realization-of-devops-%E2%80%94-when-docker-became-culture">The Realization of DevOps — When Docker Became Culture</h2><p>The concept of DevOps existed long before Docker. The idea that development and operations should collaborate to deliver software faster and more reliably had been discussed for years. However, in practice, this concept was difficult to implement. Development and operations teams used different tools, operated in different environments, and had clearly separated responsibilities. The question of “who manages the environment” often became a source of conflict, and collaboration remained limited.</p><p>Docker began to fundamentally change this situation. As environments became defined in code, developers gained a clear understanding of how their applications would run in production. More importantly, they could deliver that exact environment directly to operations teams. Operations teams no longer needed to recreate environments from scratch; they simply had to run the images provided by developers. This naturally reshaped the boundaries of responsibility between the two teams.</p><p>This transformation extended beyond technical efficiency and began to influence organizational structure and culture. Deployment cycles became shorter, and it became possible to deploy multiple times a day. Rolling back changes became simpler, and new features could be tested more rapidly. In this way, Docker turned DevOps from an abstract philosophy into a practical and functioning system.</p><p>At this point, however, another challenge began to emerge. As container-based applications increased, managing them efficiently became a new problem. Instead of dealing with a single application, systems were now composed of dozens of containers running simultaneously. The question shifted to how these containers should be orchestrated and managed at scale. Docker itself did not solve this problem, but it was the catalyst that created it. And to address this new challenge, the next stage of technology would soon emerge—Kubernetes.</p><h2 id="the-emergence-of-a-new-problem-%E2%80%94-containers-multiplied-and-management-became-necessary">The Emergence of a New Problem — Containers Multiplied, and Management Became Necessary</h2><p>The changes brought by Docker were extremely powerful, but those changes quickly gave rise to new problems. In the era when a single application ran on a single server, aligning environments was sufficient. However, as deployment became easier with Docker, developers began breaking applications into smaller pieces. Instead of one large system, it became natural to divide it into multiple smaller services. What is known as microservices architecture thus became a realistic choice.</p><p>The problem started from this point. As services were divided, deployment units also increased, and each service became an independently running container. At first, there were only a few containers, but as services expanded, their number grew rapidly. Now the problem was no longer “how to align environments,” but rather <strong>“how to operate dozens or hundreds of containers.”</strong> Running a single container was simple, but maintaining them reliably was an entirely different challenge.</p><p>Containers can fail at any time, network connections can break, and traffic can suddenly spike for a particular service. In such situations, manually managing each container becomes practically impossible. Developers were now placed in a position where they had to manage the state of the entire system, not just write code. In other words, Docker solved the deployment problem, but at the same time made operational complexity far greater.</p><p>This shift raised another critical question. Instead of running containers one by one, could the entire system be managed as a single unit? Could there be a system that automatically maintains service state, scales when needed, and recovers on its own when failures occur? Efforts to answer this question began, and in that process, a new tool emerged.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-38.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-38.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-38.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-38.png 1536w" sizes="(min-width: 720px) 720px"></figure><h2 id="the-emergence-of-kubernetes-%E2%80%94-the-world-after-docker">The Emergence of Kubernetes — The World After Docker</h2><p>The most powerful answer to this problem came from Google. Google had already been operating a massive number of services internally and had built systems to manage them long before. One of the most representative systems was Borg. Borg was designed to automatically schedule applications, efficiently use resources, and recover from failures without human intervention. This concept was later released to the public and evolved into what is now known as Kubernetes.</p><p>Kubernetes was not simply a tool for running containers. It was a system for <strong>“managing containers.”</strong> Developers no longer needed to run specific containers on specific servers. Instead, they could declare a desired state, such as “I want this application to run in multiple instances,” and Kubernetes would ensure that the system maintained that state. This approach was fundamentally different from traditional command-based operations. Systems were no longer driven by instructions about what to execute, but by definitions of what state should be maintained.</p><p>This shift had a profound impact on operations. When a service fails, it is automatically recreated. When traffic increases, the system scales up automatically. When resources are no longer needed, they are scaled down. Additionally, Kubernetes handles networking between services, meaning developers no longer need to manage low-level infrastructure details. In this sense, Kubernetes not only solved the problems created by Docker but also introduced a new level of automation.</p><p>As a result, Docker and Kubernetes became a standard combination. Docker handled the packaging of applications, while Kubernetes managed their execution and lifecycle. This combination quickly became the industry standard and formed the foundation of most modern cloud environments. More importantly, this evolution did not just represent an advancement in tools, but a redefinition of how software is designed and operated.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-39.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-39.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-39.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-39.png 1536w" sizes="(min-width: 720px) 720px"></figure><h2 id="the-birth-of-cloud-native-%E2%80%94-the-era-where-infrastructure-became-code">The Birth of Cloud Native — The Era Where Infrastructure Became Code</h2><p>The combination of Docker and Kubernetes ultimately created a new paradigm known as Cloud Native. This concept does not simply mean using the cloud. It refers to designing applications from the ground up to be optimized for cloud environments, deploying them using containers, and building systems that assume automatic scaling and recovery. In other words, Cloud Native is not just a collection of technologies, but a fundamentally new way of thinking about software.</p><p>In this paradigm, the boundary between infrastructure and application almost disappears. Servers are no longer fixed resources but entities that are created and destroyed as needed. Applications are no longer tied to specific machines but run across clusters. Developers shift their focus away from “where something runs” to “what state the system should maintain.” This transformation fundamentally changes how software systems are designed.</p><p>Furthermore, in a Cloud Native environment, deployment is no longer a special event. Continuous deployment and updates become the default, and systems are constantly evolving. Failures are no longer treated as exceptions but as expected conditions. As a result, systems are designed not to avoid failures, but to <strong>withstand and recover from them</strong>. This philosophy requires a completely different approach compared to traditional system design.</p><p>Ultimately, the transformation that began with Docker was completed through Kubernetes and took shape as Cloud Native. This progression represents not just technological advancement, but a fundamental shift in how software is built. Most modern services already operate on top of this structure, and future technologies will continue to evolve along this path. Software is no longer just a program; it has become a continuously evolving system.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-40.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-40.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-40.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-40.png 1536w" sizes="(min-width: 720px) 720px"></figure><h2 id="what-docker-left-behind-%E2%80%94-it-was-not-a-tool-but-a-paradigm">What Docker Left Behind — It Was Not a Tool, but a Paradigm</h2><p>Following the flow we have examined so far, it becomes clear that Docker represents far more than just a tool that makes containers easier to use. At first, it appeared as a solution to reduce the gap between development and production environments, but its impact extended far beyond that scope. Docker made software deployment faster, environments more stable, and blurred the boundary between development and operations. However, what matters more is that Docker <strong>changed the way developers perceive software itself</strong>.</p><p>Previously, software existed in a state where code and environment were separated. Code was managed through version control systems, but environments relied on documentation and experience. There was always uncertainty between these two domains, and as a result, deployment was perceived as a risky operation. Docker fundamentally transformed this structure. As environments became defined in code, software was no longer just code but began to be managed as a <strong>fully executable state</strong>. This shift was not merely a matter of convenience; it redefined the fundamental unit that composes software.</p><p>This transformation naturally led to the concept of Infrastructure as Code. Infrastructure was no longer something manually configured by humans, but something defined in code and automatically provisioned. Docker was the starting point of this movement, and many tools and platforms that followed evolved on top of this concept. The modern reality where servers are created in the cloud, networks are configured, and storage is connected—all through code—is a direct extension of the changes initiated by Docker.</p><p>Docker also changed how we think about development speed. In the past, deployment was slow and risky, so the best strategy was to do it as infrequently as possible. However, the combination of Docker and CI/CD transformed deployment into a fast and repeatable process. As a result, software began to be deployed more frequently and evolved more rapidly. This shift was not just a technical improvement; it influenced product development methods and organizational structures. An environment that allows rapid experimentation and iteration made it easier to validate new ideas, ultimately leading to faster innovation.</p><p>Another significant impact of Docker was standardization. As development, testing, and production environments all began to operate on the same image, concerns about environmental differences gradually disappeared. This standardization made collaboration significantly easier. Even when a new developer joined a project, they could use the exact same environment simply by running a Docker image, without going through complex setup procedures. In this sense, Docker not only solved technical problems but also transformed the way teams collaborate.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-41.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-41.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-41.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-41.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>When all of these changes are considered together, it becomes evident that Docker started as a tool but ultimately became a paradigm. While container technology existed before, Docker made it accessible and practical for everyone, thereby redefining the standards of software development. Developers no longer see themselves as tied to specific environments but instead take it for granted that applications should run consistently anywhere.</p><p>And this transformation does not stop here. On top of the standard that Docker established, Kubernetes emerged, and from there, a new world called Cloud Native was formed. Software is no longer just a program that runs—it has become a system that continuously scales and evolves. In this flow, Docker was both the starting point and a critical turning point that continues to influence the present.</p><p>In the next article, we will continue this journey by examining how Kubernetes, the technology that completed the post-Docker world, redefined the cloud. By following how Kubernetes solved the problems created by Docker and what new standards emerged as a result, the structure of modern cloud infrastructure will become much clearer.</p>]]></content:encoded>
                </item>
                <item>
                    <title>Why npm Exploded the JavaScript Ecosystem</title>
                    <link>https://en-signal.ceak.dev/00080204-npm-javascript-ecosystem-explosion-speed-network-effect/</link>
                    <guid isPermaLink="true">https://en-signal.ceak.dev/00080204-npm-javascript-ecosystem-explosion-speed-network-effect/</guid>
                    <pubDate>Thu, 16 Apr 2026 17:46:28 +0900</pubDate>
                    <dc:creator><![CDATA[ceak]]></dc:creator>
                    <category><![CDATA[⚙ Essays]]></category><category><![CDATA[📚 Defining Moments in Software History]]></category><category><![CDATA[📚 Tools That Changed Developer Culture]]></category><category><![CDATA[javascript]]></category><category><![CDATA[npm]]></category><category><![CDATA[Software Architecture]]></category>
                    <description><![CDATA[npm was not just a package manager. It transformed how code is shared, dramatically accelerated development speed, and became the infrastructure that turned JavaScript into one of the fastest-evolving ecosystems. This article analyzes how npm reshaped the very structure of software development.]]></description>
                    <content:encoded><![CDATA[<h2 id="the-limitations-of-javascript-as-a-browser-language">The Limitations of JavaScript as a Browser Language</h2><p>Early JavaScript existed in a completely different position from what we understand today as a general-purpose language. It was, at best, an auxiliary scripting language that operated strictly inside the browser, with a clearly limited role. Its primary responsibilities were handling user input, controlling simple UI interactions, and partially assisting communication with the server. Expecting anything beyond that was almost impossible. The most critical limitation was that <strong>there was no structure for sharing and reusing code at all</strong>. Developers had no choice but to implement required functionality themselves or copy and paste code written by others. In such an environment, code did not accumulate or evolve—it was repeatedly recreated and consumed in isolation.</p><p>The JavaScript ecosystem of that time would be considered primitive by today’s standards. Even when using libraries, the process did not involve installing packages but rather downloading <code>.js</code> files and manually including them in projects. The concept of version management effectively did not exist, and different versions of the same library were often mixed across projects. Dependency conflicts had not yet been formally defined, but their symptoms were already widespread. When multiple libraries were used together, global variable collisions and function name conflicts occurred frequently, making debugging an extremely difficult task. All of these problems stemmed from a single root cause: <strong>there was no standardized way to share code</strong>.</p><p>In this environment, libraries like jQuery emerged. jQuery was not merely a library—it effectively defined how JavaScript development was done. However, rather than solving the fundamental problem, it redirected it into a single dominant approach. Developers increasingly relied on jQuery for a wide range of functionality, resulting in a structure where everything converged into one massive library. While this approach improved productivity in the short term, it revealed serious limitations in scalability and maintainability over time. As codebases grew larger, they became harder to manage, and dependency on specific libraries deepened.</p><p>Ultimately, JavaScript in this era was not an ecosystem built on “small, composable modules,” but rather an environment where functionality was bundled into large monolithic libraries. Compared to other language ecosystems, this was highly unusual. Languages like Java and Python were already forming ecosystems centered around package management and modular systems, while JavaScript remained dependent on file-level copying and manual integration. This difference was not merely about developer convenience—it fundamentally <strong>limited the speed at which the ecosystem itself could grow</strong>. And this limitation would soon create pressure that pushed JavaScript into its next evolutionary stage.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-26.png" class="kg-image" alt="" loading="lazy" width="1000" height="667" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-26.png 600w, https://en-signal.ceak.dev/content/images/2026/04/image-26.png 1000w" sizes="(min-width: 720px) 720px"></figure><p>What matters here is that JavaScript’s slow growth was not due to limitations of the language itself. The real problem was the absence of proper tools and structure. While this structural limitation was somewhat tolerable within the browser environment, it became impossible to ignore once JavaScript began moving beyond it. That turning point arrived when JavaScript started to escape the browser.</p><h2 id="the-emergence-of-nodejs-and-the-need-for-server-side-javascript">The Emergence of Node.js and the Need for Server-Side JavaScript</h2><p>One of the pivotal moments that changed JavaScript’s trajectory was the emergence of Node.js. Node.js was not simply a tool that enabled JavaScript to run on servers. It was the turning point that elevated JavaScript into a fully capable programming language. Once freed from the constraints of the browser, JavaScript began to expand into much broader domains, including file system operations, network handling, and server-side logic. However, this expansion also introduced entirely new problems. Things that had previously been optional suddenly became essential.</p><p>Server-side development is inherently built upon the composition of numerous functionalities. HTTP handling, database connections, authentication, logging, and caching are just a few of the many components required. In the traditional JavaScript environment, there was no systematic way to integrate and manage these capabilities. Developers still had to write everything themselves or manually manage externally sourced code. However, this approach quickly became unsustainable in a server context. As applications grew in scale, code reuse and maintainability became critical, and <strong>a dependency management system emerged as a fundamental necessity</strong>.</p><p>At this point, JavaScript began to seriously require the concept of “packages.” It was no longer sufficient to simply execute code; developers needed a way to compose modules, manage versions, and deploy systems reliably. Node.js created this demand but did not provide the solution itself. In fact, it made the problem more visible and urgent. JavaScript was no longer a lightweight scripting language—it had become a language used to build complex systems, and it required an infrastructure capable of supporting that role.</p><p>This transformation represented more than just technical evolution—it marked a shift in development culture. Browser-centric JavaScript was typically written by individual developers for specific pages, whereas post-Node.js JavaScript moved toward team-based system development. In this transition, the most significant obstacle was <strong>how to share and reuse code effectively</strong>. Without solving this problem, JavaScript could not establish itself as a viable server-side language.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-27.png" class="kg-image" alt="" loading="lazy" width="1000" height="667" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-27.png 600w, https://en-signal.ceak.dev/content/images/2026/04/image-27.png 1000w" sizes="(min-width: 720px) 720px"></figure><p>Ultimately, Node.js opened the door to JavaScript’s potential, but it also demanded a new structure to make that potential practical. This demand naturally led to a critical question: <strong>how should all this code be managed</strong>? The answer to that question would come in the form of npm.</p><h2 id="the-birth-of-npm-not-just-a-package-manager-but-the-beginning-of-an-ecosystem">The Birth of npm: Not Just a Package Manager, but the Beginning of an Ecosystem</h2><p>npm is commonly described as a “JavaScript package manager.” However, this definition is far too superficial. At its core, npm is not merely a tool for installing packages—it is <strong>a system that created the flow and connectivity of code</strong>. If Node.js expanded JavaScript into the server domain, npm provided the foundation upon which an entire ecosystem could be built. These two are not separate concepts but rather complementary forces that complete each other.</p><p>Before npm, other languages already had their own package management systems. Python had pip, and Ruby had gem. However, npm evolved in a fundamentally different direction. The most significant difference was <strong>the structure that allowed anyone to create and publish packages with extreme ease</strong>. The barrier to entry was minimal, and all packages were connected through a centralized registry. This structure did more than provide convenience—it generated powerful network effects. As the number of packages increased, more developers were drawn into the ecosystem, and as more developers participated, the ecosystem grew even faster.</p><p>Perhaps the most important aspect of npm was that it enabled “fine-grained code sharing.” Previously, creating a library implied building something of substantial size and scope. With npm, even a few lines of code could be published as an independent package. This fundamentally changed the direction of JavaScript’s evolution. Instead of being dominated by large frameworks, the ecosystem became composed of countless small modules interconnected with one another. This shift redefined how development was approached. Developers no longer needed to implement everything themselves; instead, they solved problems by composing existing pieces.</p><p>This transformation went beyond improving productivity—it altered the very paradigm of development. The value of code was no longer measured by how much was written, but by <strong>how effectively it was composed and utilized</strong>. npm accelerated this process to an extreme degree. New ideas could be immediately implemented as packages, distributed globally, and refined collaboratively by developers around the world. The speed of this cycle was unprecedented compared to traditional software development models.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-28.png" class="kg-image" alt="" loading="lazy" width="1000" height="667" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-28.png 600w, https://en-signal.ceak.dev/content/images/2026/04/image-28.png 1000w" sizes="(min-width: 720px) 720px"></figure><p>In the end, what npm created was not just a tool, but an entire ecosystem. And this ecosystem began transforming JavaScript from a simple language into <strong>the fastest-evolving platform in software development</strong>. The next step is to examine the philosophy that drove this ecosystem’s growth, and the consequences that emerged from it.</p><h2 id="the-philosophy-of-small-modules-unix-philosophy-enters-javascript">The Philosophy of Small Modules: Unix Philosophy Enters JavaScript</h2><p>To understand the npm ecosystem, it is not enough to simply observe that “there are many packages.” What matters more is the question: <strong>why did so many packages emerge in the first place?</strong> The answer to this question is closer to a philosophical choice than a purely technical one. At some point, the JavaScript ecosystem began to follow an implicit rule. Break things down as small as possible, keep them as simple as possible, and solve problems by composing what is needed. This approach was not created by accident, but closely resembles the long-established Unix philosophy.</p><p>The core of the Unix philosophy is simple. A program should do one thing well, and multiple programs should be combined to solve larger problems. npm brought this philosophy directly into the JavaScript world. Where it was once common for a single library to contain multiple functionalities, after npm it became natural for a single package to handle only one specific task. As a result, code reusability increased dramatically, and developers began solving problems by composition rather than implementation. This shift did more than improve productivity; it <strong>changed the way developers think about building systems</strong>.</p><p>This structure aligned particularly well with the nature of JavaScript. JavaScript’s dynamic typing and flexible syntax make it well-suited for creating and distributing small modules quickly. Additionally, the web environment itself demands rapid change and experimentation, making modular design inherently advantageous. As a result, the npm ecosystem evolved not as a “finished system,” but as a <strong>continuously recomposed and reassembled structure</strong>.</p><p>However, this philosophy was not without its flaws. While composing small modules introduces simplicity at the individual level, it also creates a new kind of complexity at the system level. Each module may be simple, but once they are connected, the overall structure can become extremely complex. Developers are no longer required to understand a single library, but instead must grasp how dozens or hundreds of modules interact. At this point, the npm ecosystem moves into a new phase. Simplicity, when accumulated, begins to produce complexity—what can be described as <strong>invisible structural complexity</strong>.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-29.png" class="kg-image" alt="" loading="lazy" width="1000" height="667" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-29.png 600w, https://en-signal.ceak.dev/content/images/2026/04/image-29.png 1000w" sizes="(min-width: 720px) 720px"></figure><p>This naturally leads to the next question. As the number of small modules grows, how are their relationships managed? And what happens when those connections fall out of control? The greatest strength of npm also becomes its greatest risk.</p><h2 id="the-explosion-of-dependency-graphs-complexity-born-from-convenience">The Explosion of Dependency Graphs: Complexity Born from Convenience</h2><p>One of the most significant changes brought by npm was the dramatic increase in development speed. Problems could be solved simply by installing existing packages instead of implementing functionality from scratch. However, this convenience quickly transformed into a different kind of problem. The moment a single package is installed, its dependencies are installed alongside it, and those dependencies in turn bring in their own dependencies. This process is invisible, but in reality it forms a deep and highly complex tree structure.</p><p>Over time, this dependency structure grew exponentially. Even simple projects began to include hundreds of packages, while complex applications depended on thousands. Developers found themselves pulling in massive amounts of external code just to use a few lines of functionality. On the surface, this structure appears harmless, but in reality it resembles a system built on an unstable foundation. This is because <strong>the stability of the entire system is determined by the weakest dependency</strong>.</p><p>An even more critical issue is that most of this complexity exists outside the developer’s awareness. We tend to believe that a single <code>npm install</code> command resolves everything, but behind the scenes, countless packages are downloaded, connected, and locked into specific versions simultaneously. This process is automated, which makes it convenient, but also makes it difficult to identify the root cause of problems. Determining which package version caused an issue or which dependency introduced a conflict becomes an extremely challenging task.</p><p>This ultimately leads to the problem of “trust.” We rely on vast amounts of code that we did not write ourselves, assuming that it is safe and stable. However, this assumption can break at any moment. In open-source ecosystems, the maintenance status of a package, security vulnerabilities, or even a developer’s personal decision can impact the entire system. npm made development faster, but it also introduced a dimension in which <strong>development became more fragile</strong>.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-30.png" class="kg-image" alt="" loading="lazy" width="1000" height="667" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-30.png 600w, https://en-signal.ceak.dev/content/images/2026/04/image-30.png 1000w" sizes="(min-width: 720px) 720px"></figure><p>At this point, the npm ecosystem faces not just a technical issue, but a structural risk. And this risk becomes unmistakably visible to developers worldwide through a specific event.</p><h2 id="the-left-pad-incident-revealing-the-fragility-of-the-ecosystem">The left-pad Incident: Revealing the Fragility of the Ecosystem</h2><p>The npm left-pad incident is one of the most symbolic events for understanding the npm ecosystem. Technically, the incident itself was trivial. A package consisting of just 11 lines of code was removed from npm, and as a result, countless projects failed to build. However, what made this event shocking was not the size of the code, but its position within the dependency graph. A tiny module had become a foundational piece for an enormous number of projects, and once it disappeared, entire systems came to a halt.</p><p>This was not merely an accident, but an inevitable outcome of the npm structure. The approach of composing small modules maximizes flexibility and productivity, but it can also lead to extreme dependency concentration on specific modules. Utility functions like left-pad were widely used across numerous projects, often indirectly, meaning that a single removal triggered cascading failures. This was not simply a case of “one package being deleted,” but rather a demonstration of <strong>how deeply interconnected the entire ecosystem had become</strong>.</p><p>More importantly, this incident was not caused by a technical failure, but by a human decision. The package was removed not due to a system error, but because of the developer’s choice. This highlights that the npm ecosystem is not purely a technical infrastructure, but a structure built on people and communities. In other words, technical stability alone cannot guarantee system reliability; the social layer above it must also be considered.</p><p>Following this event, npm introduced policy changes such as restrictions on unpublishing packages. However, the most significant impact was not the policy shift, but the change in developer awareness. Many developers began to recognize the importance of dependency management, leading to the adoption of practices such as lock files, version pinning, and internal package management strategies. The left-pad incident was not just a minor disruption, but a critical moment in the maturation of the npm ecosystem.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-31.png" class="kg-image" alt="" loading="lazy" width="1000" height="667" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-31.png 600w, https://en-signal.ceak.dev/content/images/2026/04/image-31.png 1000w" sizes="(min-width: 720px) 720px"></figure><p>What this incident revealed was not just vulnerability, but the fundamental nature of npm as a system. It is fast and flexible, yet highly sensitive and deeply interconnected. And it is precisely this nature that continues to drive the rapid evolution of the JavaScript ecosystem, while simultaneously introducing new forms of risk.</p><h2 id="the-frontend-revolution-react-vue-and-npm-centered-development">The Frontend Revolution: React, Vue, and npm-Centered Development</h2><p>Despite the vulnerabilities exposed by the left-pad incident, the npm ecosystem did not stop. If anything, JavaScript began to expand even more rapidly afterward. At the center of this expansion was a fundamental shift in frontend development. In the past, frontend work was limited to combining HTML, CSS, and a small amount of JavaScript. But at a certain point, it began to transform into something entirely different. Applications became increasingly complex, requiring state management, component architecture, and build processes. And at the foundation of all of this was npm.</p><p>In particular, frameworks such as React and Vue.js could not have formed their current ecosystems without npm. These are not simply libraries, but platforms composed of countless packages and tools. Developers no longer use a single library; instead, they construct their development environments by combining Babel, Webpack, ESLint, and a wide range of plugins and utilities. All of these components are connected and distributed through npm. In other words, frontend development is no longer something that happens solely inside the browser—it has become <strong>a process that runs on top of an npm-centered development environment</strong>.</p><p>This shift fundamentally changed how development itself is performed. Previously, developers wrote and tested code directly in the browser. Now, it is standard practice to generate optimized outputs through a build process. Language extensions like TypeScript, code splitting, tree shaking, and bundle optimization are all made possible by tools that operate within the npm ecosystem. This does not merely mean that the tech stack has become more complex; it signals that <strong>frontend development has evolved into an independent engineering domain</strong>.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-32.png" class="kg-image" alt="" loading="lazy" width="1000" height="667" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-32.png 600w, https://en-signal.ceak.dev/content/images/2026/04/image-32.png 1000w" sizes="(min-width: 720px) 720px"></figure><p>At this point, npm was no longer just “a tool for installing packages.” It had become the infrastructure that drives the entire frontend ecosystem. And on top of this infrastructure, JavaScript began to establish itself not just as a scripting language, but as the fastest-evolving development platform.</p><h2 id="why-javascript-became-the-fastest-evolving-ecosystem">Why JavaScript Became the Fastest-Evolving Ecosystem</h2><p>For a long time, JavaScript was considered an “imperfect language.” Its syntax lacked consistency, its type system was loose, and it was widely believed to be unsuitable for building large-scale applications. Yet at some point, it transformed into the ecosystem that evolves faster than any other language. While improvements to the language itself played a role in this transformation, the more critical factor was the npm ecosystem. <strong>npm became the system that accelerated the process by which ideas turn into code, and code turns into standards</strong>.</p><p>In other languages, it takes a significant amount of time for new ideas or patterns to spread. There are long validation processes before something becomes part of a standard library or is recognized as an official tool. But in JavaScript, this process works in a completely different way. When a new idea emerges, it is immediately implemented and distributed as an npm package. Developers around the world can freely use it, provide feedback, and improve it. This cycle repeats at an extremely fast pace, and successful patterns naturally become de facto standards.</p><p>This structure dramatically accelerates the cycle of “experiment → diffusion → standardization.” For example, promise-based asynchronous handling, state management patterns, and component-based architectures all began as individual ideas, but spread rapidly through npm and eventually became standard practices. This demonstrates that the evolution of JavaScript is not driven solely by language designers, but by <strong>the entire community collectively shaping the language’s direction</strong>. npm functions not just as a distribution channel, but as a platform for experimentation and validation of new ideas.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-33.png" class="kg-image" alt="" loading="lazy" width="1000" height="667" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-33.png 600w, https://en-signal.ceak.dev/content/images/2026/04/image-33.png 1000w" sizes="(min-width: 720px) 720px"></figure><p>Because of this structure, JavaScript—despite its inherent weaknesses—has become the most adaptive and rapidly evolving ecosystem. And this speed does not stop at technological advancement; it evolves into a powerful network effect.</p><h2 id="npm-as-a-platform-not-code-but-a-network">npm as a Platform: Not Code, but a Network</h2><p>At this stage, it no longer makes sense to view npm as just a tool. npm is a platform, and at its core is not code, but a network. With millions of packages and countless developers interconnected, this structure goes far beyond a simple repository. If GitHub turned collaboration into a platform, npm turned the <strong>consumption of code</strong> into a platform. Developers are no longer only writing code—they are constantly integrating and composing code written by others.</p><p>This network has a powerful self-reinforcing structure. As the number of packages increases, more developers are drawn into the ecosystem. As more developers participate, even more packages are created. Through this cycle, npm has established itself as <strong>the central infrastructure of the JavaScript ecosystem</strong>. While other languages also have package management systems, few have grown as rapidly and at such scale as npm. The reason lies not just in technical excellence, but in a structure that maximizes network effects.</p><p>Moreover, npm has fundamentally changed how developers behave. In the past, solving a problem meant writing code from scratch. Now, the natural first step is to search npm for an existing package. This represents a shift in the starting point of development itself. Problem-solving has moved from “production” to <strong>“search and composition.”</strong> This transformation has significantly increased development speed, but it has also deepened the reliance on dependencies.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-34.png" class="kg-image" alt="" loading="lazy" width="1000" height="667" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-34.png 600w, https://en-signal.ceak.dev/content/images/2026/04/image-34.png 1000w" sizes="(min-width: 720px) 720px"></figure><p>Ultimately, npm was the key element that transformed JavaScript from a simple language into a platform. And this platform would later serve as the foundation for Docker, cloud computing, and modern development environments. In the next section, we will examine what npm ultimately created as a system.</p><h2 id="conclusion-what-npm-created-was-not-packages-but-speed">Conclusion: What npm Created Was Not Packages, but Speed</h2><p>Following the flow so far, one crucial fact becomes increasingly clear. The transformation brought by npm was not simply about the convenience of installing packages. It was a change at the level of redefining how development itself is done. In the past, implementing a single feature required writing code from scratch, validating it, and then making it reusable, which took a significant amount of time. However, after npm emerged, developers no longer needed to build everything from the ground up. Problems could be solved by combining the vast number of existing packages, and this process could be executed at an extremely fast pace. The essence of this change is simple. <strong>npm was a system that structurally accelerated development speed</strong>.</p><p>This speed goes far beyond merely reducing working time. It shortens the time it takes for ideas to become reality. In the past, experimenting with a new concept or architecture required substantial preparation and implementation. Now, experimentation can be achieved simply by combining a few packages. As a result, development has increasingly shifted toward an experiment-driven model, where the cost of failure is lower and the speed of success propagation is significantly higher. In such an environment, rapid iteration becomes more valuable than perfect design. npm was the tool that pushed this dynamic to its extreme.</p><p>However, this speed does not always produce positive outcomes. The dependency issues and the left-pad incident discussed earlier demonstrate how fragile such a fast-moving structure can be. Developers have become increasingly dependent on external code, and as a result, system stability has expanded into areas beyond individual control. Additionally, the rapid pace of change shortens the lifespan of technology stacks and forces continuous learning. In other words, the speed created by npm simultaneously expands both <strong>opportunity and risk</strong>.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-35.png" class="kg-image" alt="" loading="lazy" width="1000" height="667" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-35.png 600w, https://en-signal.ceak.dev/content/images/2026/04/image-35.png 1000w" sizes="(min-width: 720px) 720px"></figure><p>Despite this, the transformation brought by npm has become irreversible. JavaScript is no longer a language confined to the browser; it has evolved into a core technology spanning servers, frontend, and multiple platforms. At the center of this growth was npm. It was not just a tool, but an infrastructure that enabled development to occur on top of a network. This infrastructure became the foundation for many technologies that followed. Container-based deployment, cloud-native environments, and modern CI/CD pipelines all operate on the philosophy of building fast and deploying fast.</p><p>At this point, a natural question emerges. Why has development speed become so critical? And what technology pushed this speed even further to its extreme? If npm made it possible to build code quickly, the next step was how to execute and deploy that code even faster. It is within this flow that Docker emerged. In the next article, we will explore <strong>how Docker broke down the boundary between development and deployment, and standardized the execution environment of software itself</strong>.</p>]]></content:encoded>
                </item>
                <item>
                    <title>Readium Devlog #4 — Why Timeline Had to Become a Record</title>
                    <link>https://en-signal.ceak.dev/readium-devlog-4-timeline-as-record-not-state/</link>
                    <guid isPermaLink="true">https://en-signal.ceak.dev/readium-devlog-4-timeline-as-record-not-state/</guid>
                    <pubDate>Mon, 13 Apr 2026 22:30:59 +0900</pubDate>
                    <dc:creator><![CDATA[ceak]]></dc:creator>
                    <category><![CDATA[📱 Apps]]></category><category><![CDATA[📚 Readium Development Log — Why Design Keeps Failing]]></category><category><![CDATA[Architecture]]></category><category><![CDATA[Domain Modeling]]></category><category><![CDATA[Event Driven Design]]></category>
                    <description><![CDATA[Designing Timeline as UI data is convenient.
But that’s where records begin to break.

This post explores why Timeline had to be redefined as facts,
and how that led to thin, event-based design.]]></description>
                    <content:encoded><![CDATA[<h2 id="the-moment-it-seemed-stable-%E2%80%94-but-the-question-didn%E2%80%99t-end">The Moment It Seemed Stable — But the Question Didn’t End</h2><p>Only after organizing the session toggle did it finally feel like the structure had “settled.” Until then, it wasn’t even clear where things were going wrong, and fixing one problem would cause another to surface elsewhere. But once the policies were gathered into the UseCase, and state and calculations were separated according to their roles, at least it became visible where the system was operating. Each component’s responsibility became clearer. It was no longer a tangled mess like before. So at this point, a natural thought emerged: maybe the major problems were already behind us.</p><p>But this sense of stability didn’t last long. As soon as the structure was cleaned up, a question that hadn’t been visible before began to surface. The question was, “Where do all these changes get recorded?” When a session starts, a start event is created. When a session ends, an end event is created. When progress reaches 100%, a completion event is created. All of these are not just state changes, but records that remain on the timeline of time. And yet, how to handle these records had not been defined at all.</p><p>This question was not simply about whether to log something or store it in a database. It was a more fundamental issue. What role does this record play in the system, and in what form should it be defined? The session structure was designed to handle state, but events have a fundamentally different nature. State describes the present, but events preserve the past. How to model this difference became the core problem of the next step.</p><p>At this point, we found ourselves standing at another boundary. If organizing the session structure meant translating “policy” into code, now we had to decide how to define “record.” And this decision was not just about storage—it had the potential to shake the entire domain model once again. On top of what seemed like a stable structure, another question began to push the design forward.</p><h2 id="the-initial-way-of-seeing-timeline-%E2%80%94-the-temptation-to-store-state">The Initial Way of Seeing Timeline — The Temptation to Store State</h2><p>The first reaction to this question moved in a very natural direction. If we need to leave a record, why not store it in a form that can be used immediately? Especially when imagining the Timeline UI, this temptation becomes even stronger. What the user ultimately sees is a card-like record, and that card contains the book title, progress, session duration, and various other pieces of information. Then wouldn’t it make sense to precompute and store all of this data in advance? That way, the UI could become much simpler.</p><p>From a developer’s perspective, this approach is highly appealing. Once the data is retrieved, it can be rendered directly without additional computation or lookup. It also seems advantageous from a performance standpoint. So initially, it felt natural to attempt to design TimelineEvent not as a simple event, but as a data structure optimized for UI. In other words, to both record the event and make it immediately displayable.</p><pre><code class="language-plaintext">TimelineEvent
- bookTitle
- progressPct
- sessionDuration
- computedStats
</code></pre><p>This structure is intuitive. All the information needed for a Timeline card is contained in a single record, and it can be used as-is without additional composition. Especially in a mobile environment where reducing network and computation costs is important, this approach feels even more convincing. The idea of “if we’re going to display it anyway, let’s prepare it in advance” seems perfectly reasonable.</p><p>However, the longer you look at this structure, the more a subtle discomfort begins to emerge. Is this data really a “record”? Or is it a “result”? Although it is called TimelineEvent, it seems to store not the cause of an event, but the outcome of it. And this distinction is not just a matter of wording—it is a critical difference that can completely change the direction of the design.</p><h2 id="the-beginning-of-misalignment-%E2%80%94-when-timeline-becomes-a-%E2%80%98result%E2%80%99-instead-of-a-%E2%80%98record%E2%80%99">The Beginning of Misalignment — When Timeline Becomes a ‘Result’ Instead of a ‘Record’</h2><p>The problem with this structure becomes clear as soon as you consider real usage scenarios. For example, suppose the book title is stored inside a TimelineEvent. What happens if the user later edits the book information? Should past Timeline events remain unchanged, or should they be updated to reflect the latest information? Either choice leads to problems. If you preserve the past, the UI becomes inconsistent. If you update it, the meaning of the record is lost.</p><p>This issue is not limited to book titles. The same problem repeats if the way progress is calculated changes, if the session duration policy is modified, or even if the UI representation itself changes. Previously stored TimelineEvents contain past calculation results, so they no longer align with the new policies. Over time, the data increasingly becomes “past results that no longer match the current system.” And this misalignment only grows as time goes on.</p><p>At this point, one fact becomes clear. Derived data will eventually break. Computed values lose their meaning the moment the computation logic changes. And if you store those results directly, the system gradually accumulates more and more “dead data.” This data cannot be easily deleted, nor can it be safely modified. Because it looks like a record of the past, but in reality, it is a record of past interpretations.</p><p>Eventually, the Timeline stops being a “record” and turns into a “collection of cached results.” And in that state, the domain model is no longer trustworthy. Events are supposed to represent facts of the past, but here they represent interpretations of the past. This difference may seem small, but it is significant enough to alter the direction of the entire system.</p><p>The moment this misalignment is recognized, we are forced to return to the original question. What were we trying to preserve? And that question inevitably leads to the next decision. Timeline should not store results—it must store <strong>facts</strong>.</p><h2 id="a-shift-in-the-question-%E2%80%94-what-were-we-actually-trying-to-preserve">A shift in the question — what were we actually trying to preserve?</h2><p>As the attempt to design Timeline around results began to break down, we were inevitably pushed back to the original question. Why were we trying to build this structure in the first place? At first, the answer was simple. It was to show the records left by the user in chronological order. But once you examine that purpose more closely, it becomes clear that this is not just about “data to display,” but about “data that must be preserved.” And although those two ideas look similar, they lead in completely different directions.</p><p>Data for display is optimized for current requirements. When the UI changes, the data structure changes with it. When the representation changes, the storage format follows. On the other hand, data that must be preserved has to be independent of the UI. It must retain its meaning over time, allow new interpretations, and above all, remain unchanged. The moment this distinction becomes clear, the way we look at Timeline changes entirely. It is no longer a data structure for rendering screens, but a way for the system to record reality.</p><p>At this point, a familiar concept reappears: the distinction between state and events. State can change at any time and exists to describe the present. But events represent something that actually happened in the past, and they carry meaning on their own. We had been trying to build Timeline as a collection of state, and that led us directly into the problems of derived data. But if Timeline is instead a collection of events, the entire direction shifts. We are no longer asking “what should we show right now,” but “what actually happened.”</p><p>This shift is not a minor adjustment to the model. It is a redefinition of the domain itself. Timeline is no longer a subordinate concept of the UI, but is elevated into a core layer of the system. And at that moment, the design changes direction once again. The problem is no longer how to structure Timeline, but what we are willing to recognize as fact.</p><h2 id="the-decision-%E2%80%94-timeline-records-only-facts">The decision — Timeline records only facts</h2><p>At the end of that question, one conclusion emerges. TimelineEvent must be a fact, not state. This sentence may look simple, but in practice it requires giving up a lot. Because recording only facts means discarding most forms of convenience. It means not storing computed values, not storing UI-ready data, and even not storing certain forms of state. But without making this decision, Timeline would remain a collection of derived data.</p><p>So the structure is redefined completely. TimelineEvent is made as simple as possible. It only contains what happened, when it happened, and which entity it is connected to. Everything else is removed. At first glance, this structure feels too minimal. But that minimalism is precisely what allows an event to remain an event.</p><pre><code class="language-plaintext">TimelineEvent
- id
- recordId
- eventType
- payload
- createdAtUtc
</code></pre><p>The key element here is payload. The payload contains only the minimum information necessary to describe the meaning of the event. For example, if a session has ended, only the progress percentage at that moment is stored. Session duration or computed statistics are not stored, because they can always be derived later. The principle is clear. <strong>If it can be calculated, it should not be stored. State should not be stored. Only the cause is recorded.</strong></p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-116.png" class="kg-image" alt="" loading="lazy" width="1000" height="667" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-116.png 600w, https://en-signal.ceak.dev/content/images/2026/04/image-116.png 1000w" sizes="(min-width: 720px) 720px"></figure><p>This structure fundamentally changes the nature of Timeline. It is no longer a “convenient data store,” but a layer that records the truth of the system. And once defined this way, it becomes the part of the system that must never be altered.</p><h2 id="making-events-thin-%E2%80%94-a-design-that-chooses-not-to-store">Making events thin — a design that chooses not to store</h2><p>Making events thin is not just about reducing the number of fields. It is about deliberately choosing not to store things. In most designs, the tendency is to store as much information as possible. It feels safer, it reduces future computation, and it seems to prepare for edge cases. But this approach gradually makes the system heavier, and more importantly, it blurs the meaning of the data itself.</p><p>A thin event structure takes the opposite approach. It keeps only what is necessary and discards everything else. For example, session duration appears to be important, but in reality it can always be calculated from the start and end timestamps. If that is the case, there is no reason to store it. In fact, storing it introduces future inconsistency when the calculation logic changes. In this sense, “not storing” is not an omission, but a design decision that preserves flexibility.</p><p>At first, this approach feels uncomfortable. It gives the impression that something important is missing. But over time, its advantages become clear. Data no longer conflicts with itself, and past events retain consistent meaning. Even when new policies are introduced, there is no need to modify existing data. All changes happen at the level of interpretation.</p><p>In the end, making events thin shifts system complexity away from storage and into computation. And this shift makes the structure lighter. Heavy data cannot be undone once stored incorrectly, but computation can always be adjusted later. This simple fact becomes the most important principle in redefining Timeline.</p><h2 id="how-far-should-payload-be-allowed-%E2%80%94-between-freedom-and-control">How Far Should payload Be Allowed — Between Freedom and Control</h2><p>Even after accepting the principle of making events thin, one ambiguous area still remains. That is the payload. The structure itself is simple. Fields like id, eventType, and createdAtUtc are almost fixed, and most of the design flexibility resides inside the payload. Because of this, payload becomes both the most flexible area and the most dangerous one at the same time. If you are not careful, everything starts to go into it. And the moment that happens, you end up returning to the original “fat event” again.</p><p>At first, having payload as JSON feels extremely convenient. Even if a new event type is added, there is no need to change the schema, and you can flexibly include the necessary information. Development speed improves, and it becomes easier to experiment. But this convenience, without clear boundaries, quickly turns into something uncontrollable. You start adding progressPct, then sessionDuration, and later you feel tempted to include things like bookTitleSnapshot. As you keep adding one by one, at some point the payload starts to behave like a “hidden table.”</p><p>At this point, what matters is not “what can be included,” but rather “what must not be included.” Payload is not a space to store everything, but a space to hold only the minimum information required to reconstruct an event. For example, in the case of a SESSION_ENDED event, storing the progress at that moment is enough. Session duration can be calculated, and book information can be retrieved via recordId. In other words, payload should not be independent data, but the smallest unit that gains meaning only when combined with other data.</p><p>If this boundary is not clearly defined, payload gradually starts to invade the domain of state. And the moment state enters payload, the event is no longer an event. It becomes another form of Record. That is why payload is not an area that should be freely designed, but rather the one that must be most strictly constrained. This is where the paradox emerges: to preserve flexibility, stronger constraints are required.</p><h2 id="immutability-%E2%80%94-the-condition-for-timeline-to-be-trustworthy">Immutability — The Condition for Timeline to Be Trustworthy</h2><p>Once events are defined as facts, the next condition follows naturally. Those facts must not change. If a TimelineEvent can be modified, it is no longer a “record.” It is simply data that can be altered at any time. And at that moment, the most important property of Timeline — its <strong>trustworthiness</strong> — collapses. That is why this structure explicitly allows only the creation of events, and prohibits modification afterward.</p><p>This principle changes more than it initially appears. For example, what should happen if a user accidentally records something incorrectly? Intuitively, you might want to modify or delete that event. But doing so breaks the meaning of Timeline. Instead, a different approach is required. You need to add a new event or logically invalidate the previous one. In other words, instead of changing the past, you resolve issues by introducing new facts.</p><p>At first, this structure feels uncomfortable. The inability to modify data seems like a limitation from a developer’s perspective. But this limitation is exactly what makes the system robust. All events accumulate in chronological order, and each event carries its own meaning. And this collection of events becomes the truth of the system. Any state can be reconstructed from these events. The key point is that this reconstruction always produces the same result.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-117.png" class="kg-image" alt="" loading="lazy" width="1000" height="667" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-117.png 600w, https://en-signal.ceak.dev/content/images/2026/04/image-117.png 1000w" sizes="(min-width: 720px) 720px"></figure><p>Ultimately, immutability is not a choice, but a requirement. For Timeline to exist as a record, its data must not change. And because of this immutability, we are able to trust the data. As the system grows more complex, this trust becomes increasingly critical.</p><h2 id="how-the-ui-is-constructed-%E2%80%94-separation-of-record-and-representation">How the UI Is Constructed — Separation of Record and Representation</h2><p>Once you accept a structure where events are thin, payload is constrained, and immutability is maintained, a natural question arises. Then how is the UI constructed? In the previous structure, TimelineEvent itself contained all the data needed for UI, so it could be rendered directly. But now the situation is completely different. TimelineEvent is no longer data for UI; it is merely a record of facts.</p><p>At first, this change feels inconvenient. Additional computation and data fetching are required to build the UI. For example, to display a SESSION_ENDED event, you need to retrieve book information via recordId, and calculate session duration by combining the start and end events. In other words, a single card becomes the result of combining multiple data sources. This process appears more complex than before.</p><p>However, this structure creates a crucial separation. Record and representation are completely decoupled. TimelineEvent preserves past facts as they are, while the UI interprets those facts based on current policies and state. Because of this, even if the UI changes, there is no need to modify the data structure. If a new display format is needed, you simply change the composition logic in the ViewModel. The past events remain untouched, and only the interpretation changes.</p><p>Over time, this structure reveals even greater advantages. As features are added, UI evolves, and policies change, Timeline itself remains unaffected. All changes occur in the outer layers, while the record layer stays intact. This stability goes beyond implementation convenience and makes the entire system maintainable. And at this point, one clear conclusion emerges. Timeline is not a structure for UI, but the foundation upon which UI is built.</p><h2 id="after-the-second-collapse-%E2%80%94-what-changed-was-not-the-structure-but-the-way-of-thinking">After the Second Collapse — What Changed Was Not the Structure, but the Way of Thinking</h2><p>The process of redefining Timeline around events, constraining payload, and introducing immutability was not simply an experience of changing a model. More importantly, what changed was the way of thinking about design itself. Previously, the approach had been to store as much information as possible and utilize it later. It felt safer to have more data, as if having everything would allow flexible responses in any situation. But in reality, the opposite was true. The more we stored, the more inconsistencies emerged, and the more modifications became necessary.</p><p>After making events thin, this way of thinking completely reversed. We no longer think about “what more can be stored.” Instead, we begin to ask “what should not be stored.” This shift is not merely a change in data structure, but a change in the fundamental criteria of design. Previously, out of uncertainty, we tried to hold onto everything. Now, for the sake of trust, we deliberately leave only the minimum. This difference may seem small, but it determines the overall complexity of the system.</p><p>Through this process, a certain sense begins to emerge. A feeling that the structure has become solid. This solidity does not come from cleaner code. In fact, it is closer to the opposite. Some areas have become more complex, and there is more computation involved. But the data itself no longer wavers. Past records remain as they are, and no matter how policies change, those records do not. This stability is what anchors the design.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-118.png" class="kg-image" alt="" loading="lazy" width="1000" height="667" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-118.png 600w, https://en-signal.ceak.dev/content/images/2026/04/image-118.png 1000w" sizes="(min-width: 720px) 720px"></figure><p>Ultimately, this shift leads to a deeper question. It is no longer about “how to store,” but about “what is considered a fact.” Redefining Timeline was not a task of changing a data model, but a process of redefining how the system understands reality.</p><h2 id="timeline-is-not-ui-%E2%80%94-a-declaration-of-the-record-layer">Timeline Is Not UI — A Declaration of the Record Layer</h2><p>At this point, a declaration becomes necessary. Timeline is no longer a structure for UI. This sentence may appear simple, but in reality, it is a decision that completely separates the layers of the system. Previously, Timeline was designed based on what should be shown on the screen. Now, the direction is reversed. Timeline is responsible only for recording, and the screen is built in a separate layer that interprets those records.</p><p>This declaration is important because it clearly separates responsibilities. Timeline handles the past, and UI handles the present. These two do not interfere with each other. Even if UI policies change, Timeline does not change. And because Timeline remains structurally stable, UI can always be reconstructed in new ways. This separation is not just a structural advantage, but a source of flexibility for the entire system.</p><p>Another important change is that Timeline is no longer “data for display,” but “data for preserving meaning.” Each event is small and simple, but together they form a powerful meaning. And this meaning does not change over time. Even when new features are added, existing events remain untouched, and new interpretations are layered on top. This structure makes the system increasingly extensible.</p><p>This declaration influences all subsequent design decisions. Especially when it comes to questions like where data should be stored or how synchronization should work, this structure becomes a critical foundation. Records must not change, and interpretation must always remain flexible. This simple principle becomes the basis for everything that follows.</p><h2 id="the-next-question-%E2%80%94-where-should-the-record-live">The Next Question — Where Should the Record Live</h2><p>Once Timeline is defined as a record layer, another question naturally follows. Where should this record exist? Previously, this question did not seem very important. Whether stored on a server or locally, the data was assumed to be treated the same. But now the situation is different. Timeline is no longer just data. It is the “source of truth” of the system.</p><p>Where this truth resides is not simply a matter of storage location. It is a matter of responsibility. It determines who owns the data, where it is created, and where it is preserved. Events are generated from user actions. Then where should this data first exist? This question naturally points toward local storage. User actions occur on the device, and their records are first created there.</p><p>However, another practical problem immediately arises. Can this record remain only on the local device? Issues like backup, synchronization, and consistency across multiple devices surface at the same time. In other words, placing Timeline locally is a natural choice, but it also introduces new complexity. At this point, we arrive at another critical design crossroads.</p><p>The question now becomes clearer. Should the record live locally, or on the server? Or somewhere in between? This is not merely a technical decision, but one that defines the direction of the system. And in the process of answering this question, the concept of Local-first begins to emerge in earnest. In the next section, we will explore how this choice was made, why synchronization had to be postponed, and what lessons were learned from a Drift-based design.</p>]]></content:encoded>
                </item>
                <item>
                    <title>The Moment Stack Overflow Changed Developer Culture — How the Way We Search for Knowledge Transformed</title>
                    <link>https://en-signal.ceak.dev/00080203-stack-overflow-changed-developer-culture/</link>
                    <guid isPermaLink="true">https://en-signal.ceak.dev/00080203-stack-overflow-changed-developer-culture/</guid>
                    <pubDate>Sun, 12 Apr 2026 14:36:58 +0900</pubDate>
                    <dc:creator><![CDATA[ceak]]></dc:creator>
                    <category><![CDATA[⚙ Essays]]></category><category><![CDATA[📚 Defining Moments in Software History]]></category><category><![CDATA[📚 Tools That Changed Developer Culture]]></category><category><![CDATA[StackOverflow]]></category><category><![CDATA[Developer Culture]]></category><category><![CDATA[Knowledge System]]></category>
                    <description><![CDATA[Stack Overflow was not just a Q&amp;A website.
This platform fundamentally changed how developers solve problems, how they learn, and how they use knowledge itself.
This article analyzes how that transformation began and why it became an irreversible shift.]]></description>
                    <content:encoded><![CDATA[<h2 id="developers-before-search-%E2%80%94-why-problem-solving-was-slow">Developers Before Search — Why Problem Solving Was Slow</h2><p>Software development has always been a continuous process of problem solving. However, the method we take for granted today—copying an error message into a search engine and finding a solution within seconds—is a relatively recent phenomenon. Before the emergence of Stack Overflow, developers had to go through significantly longer and more uncertain processes to resolve issues. The problem was not simply that “there was not enough information.” Rather, information did exist, but the process of discovering it was inherently inefficient, and in many cases, it was almost a matter of luck.</p><p>At the time, developers primarily relied on mailing lists, IRC channels, and various forums. When encountering an issue with a specific library or programming language, the typical approach was to post a question on the relevant project’s mailing list and wait for a response. This process was fundamentally asynchronous. Posting a question did not guarantee an immediate answer, and it was common for days to pass without any reply. Even when responses were provided, they were often incomplete or lacked clarity and reproducibility. The questioner had to describe their situation in detail, while the responder had to infer the missing context, leading to inevitable information loss that made problem solving even more difficult.</p><p>Real-time channels like IRC offered slightly faster feedback, but they came with their own structural limitations. Conversations were ephemeral and disappeared as quickly as they occurred. Even if the same question had been discussed before, it was difficult to retrieve and reference those past discussions. As a result, knowledge was not accumulated but merely consumed in the moment. Forums were not much better. While they did contain questions and answers, they were not systematically organized or optimized for search. Older threads would become buried, new questions would repeat the same issues, and identical problems would resurface dozens of times.</p><p>In this environment, developers were constantly forced to choose between two paths. One was to spend time analyzing and solving the problem independently. The other was to search through the internet in hopes that someone had already solved it. However, even in the latter case, finding a definitive answer was far from guaranteed. Search engines were not as refined as they are today, and even knowing which keywords to use was often unclear. As a result, developers frequently defaulted to solving problems on their own, which naturally led to a <strong>structural slowdown in problem-solving speed</strong>.</p><p>Development during this period was far more isolated than it is today. Knowledge could be shared within a team, but accessing knowledge beyond that boundary required significant effort. Even if someone else had already solved the same problem, it took considerable time for that knowledge to reach another developer. This latency was not merely an inconvenience; it fundamentally limited overall development productivity and influenced how developers learned. We often think of technological progress in terms of faster tools or better performance, but in reality, <strong>changes in how we access knowledge have a far greater impact</strong>. And this period represented a time when that access mechanism was at its most inefficient.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-19.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-19.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-19.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-19.png 1536w" sizes="(min-width: 720px) 720px"></figure><h2 id="information-existed-but-was-not-accessible-%E2%80%94-the-fragmented-knowledge-structure">Information Existed, But Was Not Accessible — The Fragmented Knowledge Structure</h2><p>An interesting point is that the core problem developers faced at the time was not a “lack of information.” The internet already contained a vast amount of technical documentation, code examples, and shared experiences. The real issue was that <strong>this information was not connected within a unified structure</strong>. Knowledge was scattered across different locations, and there was no system to link them together. This was not merely an inconvenience; it created a situation where knowledge itself could not be effectively utilized.</p><p>Consider a scenario where the information needed to solve a specific error is distributed across three different sources. One part might exist in a paragraph of official documentation, another in an old personal blog post, and yet another within the archives of a mailing list. To fully resolve the issue, a developer would need to locate and synthesize all three pieces. In reality, however, navigating all these paths was extremely difficult. Search engines were not capable of effectively connecting these fragmented pieces of information, leaving much of the knowledge in a state of “existing but undiscovered.”</p><p>Another consequence of this structure was the constant repetition of identical questions. Even if a developer solved a problem and documented it in a blog post, it would not necessarily be visible enough for others to find. As a result, another developer would encounter the same issue, ask the same question, and receive a similar answer again. This cycle repeated continuously, yet the outcomes were not consolidated into a single accumulated body of knowledge. In other words, knowledge was continuously produced, but it was not accumulated. This was not just inefficiency—it was a <strong>fundamental flaw in the knowledge production system itself</strong>.</p><p>Furthermore, most information at the time was written in a context-dependent manner. Blog posts were tailored to the author’s specific situation, and forum answers were tightly bound to the original question. This required additional interpretation from the reader. Even when facing the same problem, slight differences in environment or conditions made it difficult to apply the information directly. As a result, developers constantly had to evaluate whether a given piece of information was applicable to their own situation, adding yet another layer of time and effort.</p><p>This fragmented structure imposed two simultaneous burdens on developers. One was the cost of discovering information, and the other was the cost of interpreting it. It was not enough to simply read search results; developers had to filter out reliable information and adapt it to their own context. This process was repetitive and cognitively demanding. The concept we take for granted today—where the most relevant and reliable answers naturally rise to the top—simply did not exist at the time.</p><p>Ultimately, the internet of that era functioned as a massive repository of information, yet it was also a <strong>system with an inherently inefficient knowledge structure</strong>. Information was abundant, but the interface for utilizing it was lacking. This problem was not purely technical; it had structural implications that affected the entire development culture. It was at this exact point that a fundamentally different approach became necessary. And that necessity led to the emergence of the platform that would redefine everything that followed.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-20.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-20.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-20.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-20.png 1536w" sizes="(min-width: 720px) 720px"></figure><h2 id="the-emergence-of-stack-overflow-%E2%80%94-why-it-was-more-than-just-a-qa-site">The Emergence of Stack Overflow — Why It Was More Than Just a Q&amp;A Site</h2><p>When Stack Overflow launched in 2008, many initially perceived it as just another question-and-answer website. After all, forums and Q&amp;A platforms already existed, and the introduction of a new one did not seem particularly remarkable. However, Stack Overflow was not merely an incremental improvement over existing systems—it was a <strong>complete redesign of how problems are solved</strong>. Failing to understand this distinction makes it impossible to fully grasp why this platform transformed developer culture.</p><p>The starting point of Stack Overflow was clear. The problem developers faced was not the inability to ask questions, but the difficulty of finding high-quality answers. Jeff Atwood and Joel Spolsky accurately identified the structural limitations of existing forums. Questions were abundant, but extracting valuable answers from them became increasingly difficult over time. The issue was not a lack of questions, but rather the <strong>quality and structure of answers</strong>.</p><p>To address this, Stack Overflow introduced a system that did more than simply list questions and answers—it <strong>ranked and evaluated them</strong>. Users could directly assess the usefulness of answers, and those evaluations were immediately reflected in their visibility. This approach was fundamentally different from traditional forums. Previously, all answers were treated as if they held equal value, but Stack Overflow established a system where knowledge was clearly <strong>prioritized and structured</strong>.</p><p>Additionally, the platform was designed from the outset with searchability in mind. It was not just about resolving individual questions, but about ensuring that the knowledge generated in the process could be reused by others. Questions had to be specific, answers had to be clear, and duplicate questions were consolidated. This was not merely a matter of community guidelines—it was a <strong>structural design for accumulating knowledge</strong>.</p><p>At this point, a critical shift occurred. Previously, questions and answers were consumed as one-time interactions. With Stack Overflow, they became reusable assets. A single well-crafted answer could provide value to thousands or even millions of developers. This dramatically increased the efficiency of the entire system. It was not simply an improvement in user experience—it marked a <strong>fundamental transformation in how knowledge is produced and consumed</strong>.</p><p>As a result, Stack Overflow evolved beyond being a community platform into a <strong>searchable database of collective intelligence</strong>. This structure began to fundamentally change how developers approached problem solving. The way they asked questions, searched for answers, and even learned new concepts was reshaped by this system. While the transition appeared gradual on the surface, it spread with remarkable speed. And in the next stage, developers began to exhibit entirely new patterns of behavior that had never existed before.</p><h2 id="voting-acceptance-reputation-%E2%80%94-a-structure-that-automatically-sorts-knowledge-quality">Voting, Acceptance, Reputation — A Structure That Automatically Sorts Knowledge Quality</h2><p>What fundamentally distinguished Stack Overflow from traditional forums was not merely that it gathered questions and answers in one place. The core difference lay in the fact that <strong>the system was designed to determine which answers were better on its own</strong>. Previously, on the internet, almost all information existed on the same level. Old posts and new posts, accurate answers and incorrect ones were all mixed together, and ultimately it was up to the individual user to decide what to trust. This structure effectively shifted the cost of information filtering onto the user.</p><p>Stack Overflow addressed this problem through a voting system. Users could evaluate answers using upvotes and downvotes, and the results were immediately reflected so that better answers naturally rose to the top. This mechanism was not merely about ranking popularity, but rather <strong>a method of refining information quality through collective intelligence</strong>. As numerous developers encountered the same question and evaluated answers based on their own experience, the most reliable responses naturally surfaced.</p><p>On top of this, the concept of an “accepted answer” introduced another layer of meaning. The person who asked the question could directly select the answer that solved their problem, providing a different type of signal beyond community voting. This created a dual structure where both the collective evaluation of the community and the actual resolution experience of the questioner were reflected. As a result, answers on Stack Overflow were not just “popular answers,” but <strong>answers that demonstrably solved real problems</strong>.</p><p>The reputation system also played a crucial role. When users provided high-quality answers, their reputation increased, which in turn granted them more privileges within the platform. Conversely, low-quality or incorrect answers were naturally pushed down. This was not merely a reward mechanism, but rather <strong>a system that simultaneously enforced responsibility and motivation in knowledge production</strong>. Developers were effectively putting their identity and credibility behind their answers, which helped maintain a certain level of quality across the platform.</p><p>All of these elements combined to create a knowledge structure fundamentally different from traditional forums. Information was no longer displayed in a flat, equal manner, but instead organized into a clear hierarchy. More importantly, this hierarchy was not controlled by a central authority, but continuously reshaped through user interactions. In essence, this was <strong>a self-organizing knowledge system</strong>, and it is precisely this structure that began to change developer behavior in the next stage.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-21.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-21.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-21.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-21.png 1536w" sizes="(min-width: 720px) 720px"></figure><h2 id="the-experience-of-%E2%80%9Cyou-can-just-search-it%E2%80%9D-%E2%80%94-the-moment-developer-behavior-changed">The Experience of “You Can Just Search It” — The Moment Developer Behavior Changed</h2><p>Before this shift, developers had to either ask questions, analyze code themselves, or read through entire documentation to solve problems. However, once Stack Overflow accumulated enough data, an entirely new experience became possible: <strong>searching for a problem and immediately finding that someone else had already solved it</strong>. This transformation was not just about convenience; it fundamentally changed how developers approached problem-solving.</p><p>Now, when developers encounter a problem, their first instinct is to search. They copy the error message, paste it into a search engine, and click on a Stack Overflow result near the top. What matters here is not just the act of searching, but the reliability of the results. Because of Stack Overflow’s structure, answers that appear at the top are likely to have already been validated by many others. This allows developers to make decisions quickly without going through extensive verification processes.</p><p>As this experience repeats, it begins to reshape how developers think. Instead of understanding a problem from beginning to end, the focus shifts toward <strong>finding and applying an already existing solution as quickly as possible</strong>. While this may appear negative from a purist perspective, in reality it dramatically improves productivity. Solving every problem from first principles may be ideal, but in practical development environments, it is inefficient. Stack Overflow eliminated this inefficiency and turned problem-solving into a <strong>reusable process</strong>.</p><p>This shift also expanded problem-solving from an individual experience to a collective one. Previously, only developers who had encountered a specific issue could respond quickly. Now, anyone could benefit from the shared experiences of others. This not only reduced the gap between developers but also accelerated overall development speed. In this sense, Stack Overflow evolved beyond a simple information source and became <strong>a tool that extends the cognitive capabilities of developers</strong>.</p><p>At this point, another critical shift emerges. The standard for problem-solving moves from “Do you understand it?” to “Did you solve it?” This change influences the broader development culture and leaves a lasting impact on future tools and platforms. And naturally, this transition leads to a transformation in how code itself is reused.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-22.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-22.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-22.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-22.png 1536w" sizes="(min-width: 720px) 720px"></figure><h2 id="the-transformation-of-code-reuse-%E2%80%94-the-rise-of-copy-paste-culture">The Transformation of Code Reuse — The Rise of Copy-Paste Culture</h2><p>After Stack Overflow became widespread, one noticeable shift in the development environment was the way code was reused. Previously, code reuse was primarily done at the level of libraries or frameworks. Developers would adopt well-built modules and integrate them into their systems in a structured way. However, after Stack Overflow, an entirely different form of reuse emerged: <strong>snippet-level reuse</strong>, or what is commonly referred to as copy-and-paste.</p><p>This shift is not merely about convenience. A change in the unit of code reuse implies a change in how developers perceive problems. Developers are no longer only thinking in terms of full system design, but also in terms of quickly finding and applying small pieces of code that solve specific issues. Stack Overflow contains an enormous number of such snippets, many of which are already validated by others. This enables developers to <strong>assemble solutions by combining only the necessary parts</strong>, rather than building everything from scratch.</p><p>Of course, this change has been criticized. The term “copy-paste developer” emerged as a way to describe those who use code without fully understanding it. Indeed, applying code without considering its context can introduce bugs and increase long-term maintenance costs. However, it is equally true that this approach has significantly increased development productivity. Writing every piece of code from scratch may be ideal, but it is not realistic in most environments. Stack Overflow effectively removed this inefficiency and established <strong>a culture of reusing existing solutions</strong>.</p><p>This transformation also affects the role of the developer. In the past, deep understanding of algorithms and internal mechanisms was essential for solving problems. Today, however, <strong>the ability to quickly find appropriate solutions, apply them, and adapt them when necessary</strong> has become more important. This shift moves developers away from being mere code writers and toward becoming designers who assemble and orchestrate solutions.</p><p>Ultimately, Stack Overflow redefined how code is reused. The shift moved from library-centric reuse to problem-centric reuse. And this transformation does not remain confined to code alone; it extends into how developers learn and think. Developers are no longer expected to master everything themselves, but rather to efficiently pull in the knowledge they need at the right moment. This trend becomes even more apparent in the next stage.</p><h2 id="the-way-developers-learn-changed-%E2%80%94-from-documentation-to-examples">The Way Developers Learn Changed — From Documentation to Examples</h2><p>One of the quiet yet fundamental changes that occurred after Stack Overflow became widespread was the way developers learn. In the past, learning followed a clear path. Developers would read official documentation, study concepts through books, and gradually build understanding by following structured examples. This process was systematic, but it was also slow, and it often took a significant amount of time before one could actually solve real problems. In contrast, learning after Stack Overflow became far more immediate and situational. Developers no longer start by learning concepts first. Instead, they encounter a problem and acquire the necessary knowledge in reverse while solving that problem.</p><p>This shift goes beyond mere speed; it changes the structure of learning itself. Previously, one would follow a curriculum with the goal of “learning a language.” Now, the starting point is “solving this error.” From there, developers absorb only the specific concepts needed to resolve the issue. This approach is often referred to as <strong>problem-driven learning</strong>, and Stack Overflow made it extremely efficient. When an error message is searched, the platform provides just enough information to resolve the issue, allowing developers to move forward without fully mastering the entire conceptual background.</p><p>In this process, the depth of understanding becomes less important than the speed of application. Even without fully grasping every concept, being able to quickly learn and apply the necessary parts is often sufficient. While this approach might be criticized in traditional learning models, it aligns closely with real-world development environments. Deadlines and requirements are always present, and it is unrealistic to expect developers to deeply understand everything. Stack Overflow reflects this reality by creating a structure that <strong>delivers knowledge exactly when it is needed</strong>.</p><p>This transformation also affects how developers remember information. In the past, it was important to store knowledge internally. Now, it is more important to know “where the knowledge exists.” In other words, the focus of memory shifts from knowledge itself to the <strong>location of knowledge</strong>. This turns developers into explorers. Solving problems no longer requires knowing everything, but rather forming the right questions and quickly finding the corresponding answers.</p><p>This learning model influences many tools and platforms that followed. Even official documentation has increasingly become example-driven, and tutorials are now structured around solving real problems. Stack Overflow did not simply provide information; it <strong>redefined how developers learn</strong>. And this shift naturally affects how knowledge is structured and where it accumulates.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-23.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-23.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-23.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-23.png 1536w" sizes="(min-width: 720px) 720px"></figure><h2 id="the-new-standard-created-by-stack-overflow-%E2%80%94-the-de-facto-infrastructure-of-developer-knowledge">The New Standard Created by Stack Overflow — The De Facto Infrastructure of Developer Knowledge</h2><p>Once Stack Overflow reached a certain scale, it became difficult to view it as just another website. Instead, it began to function as the <strong>core infrastructure where developer knowledge is stored and accessed</strong>. While this transformation appeared gradual, it actually spread very rapidly. In particular, its integration with search engines made Stack Overflow the starting point for nearly every development problem. No matter what error was searched, Stack Overflow consistently appeared among the top results, and this behavior quickly became ingrained in developers’ workflows.</p><p>What is crucial here is that Stack Overflow does not merely provide information. It operates as a <strong>knowledge interface tightly coupled with search</strong>. Search engines like Google index vast amounts of data, but determining which information is actually useful is a separate challenge. Stack Overflow already provides internally ranked and validated content, giving it a high level of trustworthiness in search results. This naturally drives more traffic, which in turn leads to more data accumulation, creating a powerful feedback loop.</p><p>This structure has a direct impact on the entire development ecosystem. When a new library or technology emerges, questions and answers about it begin to accumulate on Stack Overflow. As this data grows, the technology becomes easier to approach. Conversely, technologies with limited presence on Stack Overflow face higher barriers to entry. This goes beyond a simple difference in available information—it becomes a factor that <strong>directly influences technology adoption and diffusion speed</strong>.</p><p>Stack Overflow also begins to function as a kind of unofficial standard. The most upvoted solution to a problem is often treated as the “correct answer,” forming a shared baseline among developers. Even when official documentation exists, Stack Overflow answers are often more widely used in practice. This creates an interesting inversion. While documentation is supposed to be the authoritative source, in reality, <strong>answers shaped by collective experience become the dominant standard</strong>.</p><p>Ultimately, Stack Overflow evolves into more than a community—it becomes a <strong>collective memory system for developer knowledge</strong>. Instead of relying on individual experience, millions of experiences are aggregated into a single database and reused in real time. This represents a scale and form of knowledge infrastructure that had never existed before. However, this same structure also introduces new challenges.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-24.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-24.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-24.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-24.png 1536w" sizes="(min-width: 720px) 720px"></figure><h2 id="limitations-and-shadows-%E2%80%94-elitism-duplicate-questions-and-barriers-to-entry">Limitations and Shadows — Elitism, Duplicate Questions, and Barriers to Entry</h2><p>As with any system that grows at scale, side effects inevitably emerge. Stack Overflow is no exception. In fact, because its structure is so well-designed, its issues become even more visible. One of the most prominent problems is its lack of friendliness toward beginners. Questions that are unclear or considered duplicates are quickly closed or removed. While this is a structural mechanism to reduce redundancy, it simultaneously acts as a <strong>high barrier to entry for new users</strong>.</p><p>The reputation system is also a double-edged sword. Users with high reputation gain more authority, but this can lead to a situation where a specific group of users influences the direction of the community. In some cases, certain types of answers or ways of thinking are overly emphasized, while alternative approaches are marginalized. This can ultimately result in <strong>a limitation of diversity in knowledge</strong>. In rapidly evolving technological environments, even outdated answers may continue to dominate simply because they were once highly upvoted.</p><p>The strict handling of duplicate questions is another source of controversy. While repeated questions are inefficient, the context of each question is often slightly different. However, within Stack Overflow’s structure, these subtle differences are not always fully recognized and are frequently treated as simple duplicates. This can hinder not only beginners but also those exploring new or nuanced problems. In effect, the process of refining knowledge can simultaneously act as a <strong>constraint on knowledge expansion</strong>.</p><p>Despite these issues, it is impossible to separate them from the core value that Stack Overflow provides. Maintaining high-quality information requires a certain level of rules and filtering, and the friction that arises from this process is, to some extent, inevitable. The key is to understand that this structure is not perfect. Stack Overflow is not a system that completely solved the problem of knowledge—it is simply a system that <strong>reconstructed it in a far more efficient way than before</strong>.</p><p>At this point, an important question begins to emerge. If even this structure is no longer optimal, what comes next? What if there were a way to move beyond searching and selecting knowledge altogether? And that question leads directly to the transformation we are now beginning to experience.</p><h2 id="and-now-%E2%80%94-is-ai-replacing-stack-overflow">And Now — Is AI Replacing Stack Overflow</h2><p>We are now standing at yet another turning point. In the past, developers searched to solve problems, and as a result, they would read answers on Stack Overflow. However, that very step of searching is now beginning to disappear. Instead of finding someone else’s answer, it has become possible to <strong>generate an answer instantly</strong> by simply inputting a question. This shift is not just a change in interface, but a signal that the entire structure of problem-solving is being reorganized once again.</p><p>Stack Overflow was a system that connected questions to answers. Questions existed, answers accumulated, and others reused them over time. However, AI-based systems do not follow this process. Instead, they generate answers in real time based on vast amounts of previously learned data. What matters here is that answers are no longer tied to a specific document or thread. In other words, the paradigm shifts from “finding where the answer exists” to <strong>creating the answer at the moment it is needed</strong>.</p><p>This transformation has an immediate impact on developer behavior. There is no longer a need to carefully craft search keywords or compare multiple pages to determine the best answer. Instead, developers can describe a problem in natural language and receive a solution in the form of a sentence or code. This can be seen as a further compression of the experience that Stack Overflow once provided. Where the process used to be question → search → selection → application, it is now reduced to <strong>question → generation → application</strong>.</p><p>However, this shift does not mean the end of Stack Overflow. In fact, the opposite is closer to the truth. The answers generated by AI are based on previously learned data, and a significant portion of that data originates from platforms like Stack Overflow. In this sense, Stack Overflow continues to serve as a <strong>foundational knowledge database</strong>. The difference is that a new interface has been layered on top of it. Users may no longer directly access that database, but its influence remains deeply embedded.</p><p>Moreover, AI-generated answers are not always accurate or optimal. At times, they may confidently present incorrect information or produce flawed code in certain contexts. In such cases, verified knowledge sources like Stack Overflow still play a crucial role. Many developers cross-check AI-generated responses by searching or referring back to Stack Overflow. This demonstrates that the two systems are not competitors, but rather exist in a <strong>complementary relationship</strong>.</p><p>Ultimately, the current transformation is not about replacing Stack Overflow, but about adding a new layer on top of it. Problem-solving is becoming faster, and access is becoming more intuitive. However, the foundation remains rooted in accumulated knowledge. This evolution is not just a change in tools, but a fundamental shift in how developers interact with knowledge itself.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-25.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-25.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-25.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-25.png 1536w" sizes="(min-width: 720px) 720px"></figure><h2 id="conclusion-%E2%80%94-knowledge-is-no-longer-learned-it-is-invoked">Conclusion — Knowledge Is No Longer Learned, It Is Invoked</h2><p>If we follow the flow up to this point, we arrive at a clear conclusion. The essence of the change brought by Stack Overflow was not simply about making information easier to find. It was about <strong>transforming the very way knowledge is handled</strong>. In the past, accumulating and understanding knowledge was essential. Developers were expected to learn as much as possible and solve problems based on that internalized knowledge. However, in the trajectory that begins with Stack Overflow and continues into the era of AI, this assumption is steadily being challenged.</p><p>What matters now is not how much you know, but <strong>what you can retrieve at the moment you need it</strong>. Developers are no longer entities that must remember everything. Instead, they are becoming individuals who define problems and rapidly retrieve and apply the appropriate solutions. This is not merely a technological shift, but a shift in thinking itself. Knowledge is no longer a personal asset stored in one’s mind, but an external resource that can be invoked at any time.</p><p>This transformation redefines the role of the developer. Beyond the ability to write code, the ability to structure problems and formulate precise questions becomes increasingly important. The quality of the question directly determines the quality of the answer, and this ultimately impacts the quality of the outcome. In this sense, developers are evolving from “producers of knowledge” into <strong>orchestrators who assemble and apply knowledge</strong>. Stack Overflow marked the beginning of this shift, and AI is now pushing it to its extreme.</p><p>However, viewing this transition purely from the perspective of efficiency can be misleading. The ability to invoke knowledge does not eliminate the value of understanding. On the contrary, as problem-solving becomes faster, developers gain the opportunity to tackle deeper and more complex challenges. The critical factor is not the tool itself, but how it is used. Just as Stack Overflow did, AI serves not to replace the developer’s capability, but to <strong>extend it</strong>.</p><p>Ultimately, the progression outlined in this article represents a continuous transformation. The slow, fragmented knowledge exchange of mailing lists and forums evolved into a structured and accumulated system through Stack Overflow, and is now transitioning into a model of instant generation through AI. This evolution will not stop. It is highly likely that another form of interface will emerge in the next stage.</p><p>In the next article of this series, we will examine how this structure of knowledge reuse triggered an explosive transformation in the code ecosystem. In particular, we will explore how the emergence of npm expanded the JavaScript ecosystem, and how code reuse reshaped the industry at large.</p>]]></content:encoded>
                </item>
                <item>
                    <title>Why the Same Command Produces Different Results — The Decision Model Behind Linux Execution</title>
                    <link>https://en-signal.ceak.dev/linux-execution-decision-model/</link>
                    <guid isPermaLink="true">https://en-signal.ceak.dev/linux-execution-decision-model/</guid>
                    <pubDate>Sun, 12 Apr 2026 10:22:34 +0900</pubDate>
                    <dc:creator><![CDATA[ceak]]></dc:creator>
                    <category><![CDATA[🎯Docs]]></category><category><![CDATA[📚 Complete Guide to Linux Shell Execution]]></category><category><![CDATA[Linux]]></category><category><![CDATA[Execution Model]]></category><category><![CDATA[Debugging]]></category>
                    <description><![CDATA[In Linux, identical commands can produce different results because execution is determined not by input, but by a layered decision model. This article analyzes how PATH, shebang, environment variables, and file descriptors shape outcomes through state and selection—not just execution flow.]]></description>
                    <content:encoded><![CDATA[<h2 id="execution-results-are-not-determined-by-input">Execution results are not determined by input</h2><p>In Linux, it is common for the same command to produce different results. This phenomenon is not an exception but a structural outcome. In many cases, users assume that input determines execution. However, in reality, input is only the starting condition of execution. The execution result is determined not by the input but by the system state. Without understanding this distinction, it is impossible to explain why the same command produces different results.</p><p>Input is simply a string. A string is not an executable object. The system must interpret this string and transform it into an executable structure. In this process, the input is combined with various environmental factors. The result of this combination affects both the execution target and the execution method. Therefore, the input itself does not guarantee the result. Input is merely part of the decision process.</p><p>If you verify this structure directly, it becomes clear why input-centric thinking is incorrect.</p><pre><code class="language-bash">$ which python
/usr/bin/python

$ mkdir -p /tmp/testbin
$ echo -e '#!/bin/sh\necho fake python' &gt; /tmp/testbin/python
$ chmod +x /tmp/testbin/python

$ PATH=/tmp/testbin:$PATH
$ python
fake python
</code></pre><p>In this experiment, the input is always <code>python</code>. However, the result is completely different. Initially, <code>/usr/bin/python</code> is executed. Afterward, <code>/tmp/testbin/python</code> is executed. The input has not changed, but the result has. This change is caused by the environmental factor called PATH.</p><p>This result reveals an important fact. The execution result is not determined by the input string, but by <strong>the environment that interprets the input</strong>. Therefore, to understand execution, one must look not at the input but at the interpretation structure. In the next step, this interpretation is explained not as a single decision, but as an accumulation of multiple choices.</p><h2 id="execution-is-not-a-single-decision-but-multiple-selections">Execution is not a single decision, but multiple selections</h2><p>Execution appears to be a single event. However, in reality, it is the result of multiple selections occurring sequentially. Each selection is based on the result of the previous one. This structure makes execution complex. At the same time, this structure makes execution results predictable. The difficulty is not that prediction is impossible, but that the criteria are not clearly understood.</p><p>Each selection is a process of choosing one option among multiple candidates. For example, the same name may exist in multiple paths. The system selects one of them. Even if a file exists, there may be multiple ways to execute it. The system selects one of those as well. In this way, execution is not a single path but a chain of selections.</p><p>This selection structure creates confusion because it is not visible. However, parts of it can be observed.</p><pre><code class="language-bash">$ type -a ls
ls is /bin/ls
ls is /usr/bin/ls
</code></pre><p>This result shows that the same name can exist in multiple locations. However, during actual execution, only one is selected. Which one is selected is determined by rules. These rules are not explicitly visible to the user. However, they directly affect the result.</p><p>Another example is alias.</p><pre><code class="language-bash">$ alias ls='echo override'
$ ls
override
</code></pre><p>In this case, the same input <code>ls</code> produces a completely different behavior. This change occurs because the execution target has changed. This is also the result of a selection. The system selected an alias instead of a file.</p><p>The core of this structure is clear. Execution is not a “fixed path,” but a <strong>selected path</strong>. And this selection occurs in multiple stages. If the selection criteria at each stage differ, the result also differs. Therefore, to understand execution results, one must understand how selections are made. In the next step, the most important selection criterion, PATH, is analyzed.</p><h2 id="path-is-not-a-search-but-a-%E2%80%9Cpriority-system%E2%80%9D">PATH is not a search, but a “priority system”</h2><p>In many explanations, PATH is described as a simple search list. However, this explanation is insufficient. PATH is not merely a mechanism for finding files. PATH is a rule for selecting one among multiple candidates. In other words, PATH is not a search, but a priority system.</p><p>PATH defines the order of directories. This order is not a simple listing. It represents selection priority. If executables with the same name exist in multiple locations, the one in the earlier directory in PATH is selected. This selection stops at the first match. Subsequent candidates are not considered.</p><p>This structure becomes clear through experimentation.</p><pre><code class="language-bash">$ echo $PATH
/usr/bin:/bin

$ which ls
/usr/bin/ls

$ PATH=/bin:/usr/bin
$ which ls
/bin/ls
</code></pre><p>In this case, the input <code>ls</code> is the same. However, as the PATH order changes, the selection result changes. The executed file is different. This change is not just a difference in location. Different versions of the program may be executed. Therefore, the result itself may differ.</p><p>This structure makes PATH not just a configuration value, but a <strong>policy that determines execution results</strong>. Changing PATH is equivalent to changing the execution environment. Without understanding this, it is impossible to explain why execution results differ.</p><p>Additionally, PATH is not explicit. Users often do not check PATH directly. However, PATH always affects execution. Because of this hidden influence, execution results appear difficult to predict. In reality, they are not unpredictable—the criteria are simply hidden.</p><p>At this point, a structure becomes visible. The input is fixed. However, if the selection criteria change, the result changes. This selection does not end at the file path level. In the next step, how the selected file is actually interpreted will be examined.</p><h2 id="a-file-is-not-an-execution-target-but-an-interpretation-target">A file is not an execution target, but an <strong>interpretation target</strong></h2><p>Even if a specific file path is selected through PATH, execution is not yet determined. What is obtained at this point is simply a file. However, not all files are executed in the same way. The system must determine again how this file should be processed. In other words, a file is not an execution target, but an <strong>interpretation target</strong>.</p><p>Files are handled differently depending on their form. Most notably, binaries and scripts follow completely different paths. A binary is a format that the system can execute directly. In contrast, a script is just text. Text itself cannot be executed. Therefore, additional interpretation is required for execution. This difference fundamentally changes the execution method.</p><p>This distinction becomes evident when examining the internal structure of the file.</p><pre><code class="language-bash">$ file /bin/ls
/bin/ls: ELF 64-bit LSB executable

$ echo -e '#!/bin/sh\necho hello' &gt; test.sh
$ chmod +x test.sh

$ file test.sh
test.sh: POSIX shell script, ASCII text executable
</code></pre><p>In the above result, <code>/bin/ls</code> is in ELF format. This format can be directly understood by the kernel. In contrast, <code>test.sh</code> is a text file. Although it is marked as executable, it is not actual executable code. Therefore, the system does not execute this file directly.</p><p>This difference directly affects execution results. Even with the same execution permission, the processing method differs. A binary is executed directly. A script is executed through another program. In other words, the execution target is not the file itself, but the <strong>interpreted result of the file</strong>.</p><p>This structure reveals an important fact. Execution is not “executing a file.” Execution is “executing the result of interpreting a file.” Therefore, what the file is matters less than <strong>how it is interpreted</strong>. In the next step, the structure that enforces this interpretation, the shebang, is analyzed.</p><h2 id="shebang-changes-the-execution-subject">Shebang changes the execution subject</h2><p>A script file can be executed because of the shebang. However, the shebang is not just syntax. It is a rule that redefines the execution target. In other words, instead of executing the file itself, it is a mechanism that <strong>causes another program to be executed</strong>.</p><p>The shebang is located on the first line of the file. This line, which starts with <code>#!</code>, specifies the interpreter path. Based on this information, the system determines the execution method again. In this process, the original execution target is not preserved. Instead, a new execution target is selected. That is, the execution subject changes.</p><p>This structure becomes clear through experimentation.</p><pre><code class="language-bash">$ echo -e '#!/bin/sh\necho shell' &gt; test.sh
$ chmod +x test.sh
$ ./test.sh
shell
</code></pre><p>This file appears to be executed directly. However, in reality, <code>/bin/sh</code> is executed. <code>test.sh</code> is not the execution target but is passed as input.</p><p>To make this distinction clearer, if you change the shebang, the result also changes.</p><pre><code class="language-bash">$ echo -e '#!/bin/echo\nhello world' &gt; test.sh
$ chmod +x test.sh
$ ./test.sh
./test.sh
</code></pre><p>Here, <code>echo</code> is executed. And <code>test.sh</code> is passed as an argument. In other words, the content of the file is not executed. What is executed is <code>echo</code>. The shebang completely changes the execution subject.</p><p>This structure shows that execution is not fixed but <strong>reinterpretable</strong>. Even with the same file, different shebangs produce completely different results. Therefore, the execution target is not the file path, but the <strong>reinterpreted execution structure</strong>.</p><p>At this stage, an important conclusion emerges. Execution is not determined once. Execution is redefined multiple times. And these redefinitions change the result. In the next step, we analyze how this reinterpreted execution is applied on top of the existing state.</p><h2 id="execution-does-not-start-from-scratch-%E2%80%94-it-changes-on-top-of-an-existing-state">Execution does not start from scratch — it changes on top of an existing state</h2><p>In many explanations, program execution is described as “a new process starts.” However, this expression hides an important structure. In reality, execution always occurs on top of an existing state. In other words, execution is not creation but a <strong>state transformation</strong>. Without understanding this distinction, it is impossible to explain post-execution behavior.</p><p>Before execution, an execution context already exists. This context includes the current running state. A new execution does not completely discard this state. Some parts are preserved, and only certain parts are changed. In particular, the state managed by the kernel is preserved. Representative examples include file descriptors and environment variables.</p><p>This structure can be verified through a simple experiment.</p><pre><code class="language-bash">$ echo -e '#include &lt;unistd.h&gt;\nint main(){write(1,"hello\n",6);}' &gt; test.c
$ gcc test.c -o test

$ ./test &gt; out.txt
$ cat out.txt
hello
</code></pre><p>Here, the <code>test</code> program sends data to standard output. However, the actual output is saved to a file. This happens because the output structure configured before execution is preserved. The program is unaware of this. But the result changes.</p><p>This phenomenon reveals an important fact. Execution is not completely determined by the code. The execution result is influenced by the <strong>pre-existing state</strong>. Even if the code is the same, different states produce different results.</p><p>This structure also shows that execution is not something that “starts completely anew.” Execution is a process in which new code is applied on top of an existing context. Because of this, structures defined in earlier stages are preserved as they are.</p><p>In conclusion, execution is not an isolated event. Execution is a transformation based on an existing state. This transformation preserves elements determined in previous stages. Therefore, to understand execution results, one must consider not only the code but also the state.</p><h2 id="the-inputoutput-structure-distorts-execution-results">The input/output structure distorts execution results</h2><p>It is easy to think that a program’s result is determined by its code. However, in reality, even the output result is not determined by code alone. A program merely sends data to specific file descriptors. Where those descriptors point to is already determined before execution. Therefore, even with identical code, different input/output structures produce different results.</p><p>File descriptors are handles represented as numbers. Standard input is 0. Standard output is 1. Standard error is 2. These descriptors point to specific resources. By default, they point to the terminal. However, this connection is not fixed. It can be changed to another resource before execution. This change is redirection.</p><p>This structure can be verified through a simple experiment.</p><pre><code class="language-bash">$ echo hello
hello

$ echo hello &gt; out.txt
$ cat out.txt
hello
</code></pre><p>The first command outputs to the terminal. The second command outputs to a file. The code itself is identical. <code>echo</code> sends data to standard output in the same way. However, the results appear completely different. This difference occurs because the output target has changed.</p><p>This structure works the same way even in more complex forms.</p><pre><code class="language-bash">$ ls /notfound &gt; out.txt 2&gt;&amp;1
$ cat out.txt
ls: cannot access '/notfound': No such file or directory
</code></pre><p>Here, even the error output is redirected to the file. <code>2&gt;&amp;1</code> connects standard error to the same destination as standard output. This connection is also determined before execution. The program does not recognize this. However, the result changes completely.</p><p>This structure reveals an important fact. Execution results are not the result of code alone. Execution results are the <strong>combined result of code and the input/output structure</strong>. Therefore, if the output appears differently than expected, you must first examine the structure, not the code. In the next step, we examine environment variables, which modify this structure more subtly.</p><h2 id="environment-variables-change-execution-invisibly">Environment variables change execution invisibly</h2><p>Environment variables are not explicitly visible during execution. However, they continuously affect execution results. In particular, many programs change their behavior based on environment variables. This change occurs without modifying the code. Therefore, environment variables are elements that change execution results “invisibly.”</p><p>Environment variables are not just configuration values. They are part of the execution context. When a program starts, it receives environment variables. It then determines its behavior based on these values. This process is difficult to observe from the outside. However, it directly affects the result.</p><p>This structure can be verified through a simple experiment.</p><pre><code class="language-bash">$ echo 'echo $MY_VAR' &gt; test.sh
$ chmod +x test.sh

$ ./test.sh

$ MY_VAR=hello ./test.sh
hello
</code></pre><p>In the first execution, nothing is printed. In the second execution, <code>hello</code> is printed. The input is the same. The file is also the same. However, the result is different. This difference is caused by the environment variable.</p><p>Environment variables also affect the selection of the execution target. PATH is also an environment variable. Therefore, environment variables change both the execution target and the execution result simultaneously. This influence occurs outside the code. Therefore, it is impossible to predict execution results based on code alone.</p><p>Additionally, environment variables can be applied globally. They are defined by shell configuration files or system settings. In this case, users may not be aware of the environment in which they are executing. This hidden state changes execution results.</p><p>In conclusion, execution cannot be explained by code and input alone. Execution is determined by the entire state, including the environment. Because this state is not visible, execution results appear unstable. In the next step, we analyze how these differences manifest as errors.</p><h2 id="execution-failure-is-not-random-but-a-%E2%80%9Cdecision-failure%E2%80%9D">Execution failure is not random, but a “decision failure”</h2><p>Execution failures often appear unpredictable. The same command works in one environment but fails in another. This phenomenon appears random. However, in reality, it is the result of a failure at a specific stage of decision-making. Execution is a chain of multiple selections. Failure occurs at a particular point within this chain.</p><p>The most common failure occurs at the stage of selecting the execution target.</p><pre><code class="language-bash">$ unknown_command
bash: unknown_command: command not found
</code></pre><p>In this case, the system failed to find an execution target. This is not an input problem, but a selection rule problem. The name does not exist in PATH. Therefore, execution does not start.</p><p>Next, failure can occur at the stage of determining the execution method.</p><pre><code class="language-bash">$ echo 'echo hello' &gt; test.sh
$ chmod +x test.sh
$ ./test.sh
bash: ./test.sh: cannot execute: Exec format error
</code></pre><p>In this case, the file exists and has execution permission. However, the execution method is not defined. This is because there is no shebang. The system cannot determine how to execute this file. Therefore, execution fails.</p><p>Another failure occurs due to permission issues.</p><pre><code class="language-bash">$ chmod -x test.sh
$ ./test.sh
bash: ./test.sh: Permission denied
</code></pre><p>In this case, both the execution target and execution method are determined. However, the execution conditions are not satisfied. Therefore, execution is blocked.</p><p>All of these failures share a common characteristic. It is not that execution itself has failed. <strong>One of the decision processes has failed</strong>. Therefore, to understand execution failure, you must identify which stage of the decision process failed.</p><p>Once this structure is understood, execution problems are no longer ambiguous. Each stage has a clear role. Failures correspond to those roles. Therefore, problems can be analyzed step by step. This perspective turns execution into a debuggable structure.</p><h2 id="execution-is-not-code-execution-but-the-%E2%80%9Cresult-of-a-decision-structure%E2%80%9D">Execution is not code execution, but the “result of a decision structure”</h2><p>When all the previous content is synthesized, it converges into a single conclusion. Execution is not an event where code is executed. Execution is the result of code being applied on top of an already determined structure. In other words, the essence of execution is not code, but the <strong>decision structure</strong>. Without this perspective, it is impossible to explain execution results consistently.</p><p>In general, execution is understood as “running a program.” This expression is simple, but it omits important elements. Before execution, multiple stages of selection have already taken place. The execution target is selected. The execution method is determined. The execution subject is redefined. The execution context is constructed. The input/output structure is established. Only after all these processes are completed is the code applied. Therefore, code execution is merely the final step.</p><p>To verify this structure experimentally, you only need to keep the code the same and change the execution environment.</p><pre><code class="language-bash">$ echo -e '#include &lt;stdio.h&gt;\nint main(){printf("hello\n");}' &gt; test.c
$ gcc test.c -o test

$ ./test
hello

$ ./test &gt; out.txt
$ cat out.txt
hello
</code></pre><p>In both cases, the same code is executed. However, the form of the result is different. The first outputs to the terminal. The second is saved to a file. There is no difference in the code. The input is also the same. The reason the results differ is that the execution structure is different. This difference was already determined before execution.</p><p>This structure changes the <strong>frame of reference</strong> for viewing execution. To understand execution results, analyzing code alone is not sufficient. You must examine what selections were made before execution. You must check what PATH is. You must check how environment variables are configured. You must check how the input/output structure is connected. All of these elements combine to produce the result.</p><p>This perspective also extends to the entire system. Service execution, container execution, and pipeline composition follow the same structure. The execution target is defined. The execution environment is constructed. The execution structure is established. Then the code is applied. In other words, execution is not a problem of an individual program, but a pattern repeated across the entire system.</p><p>The core of this pattern is consistency. Regardless of the input, it is interpreted and decided in the same way. Only the values selected at each stage differ. Because of this, execution is predictable. The reason it appears difficult to predict is that the structure is complex. Once the structure is understood, the result becomes explainable.</p><p>In conclusion, execution is no longer “the act of running a program.” Execution is the <strong>final result of a decision structure based on state</strong>. From this perspective, execution is no longer a black box. Each stage has a clear role. The choices at each stage are directly reflected in the result. Once this connection is understood, execution results can be predicted even in new environments.</p>]]></content:encoded>
                </item>
                <item>
                    <title>Readium Devlog #3 — Why Session Toggle Was Never Simple</title>
                    <link>https://en-signal.ceak.dev/readium-devlog-3-session-toggle-domain-breakdown/</link>
                    <guid isPermaLink="true">https://en-signal.ceak.dev/readium-devlog-3-session-toggle-domain-breakdown/</guid>
                    <pubDate>Sat, 11 Apr 2026 20:21:42 +0900</pubDate>
                    <dc:creator><![CDATA[ceak]]></dc:creator>
                    <category><![CDATA[📱 Apps]]></category><category><![CDATA[📚 Readium Development Log — Why Design Keeps Failing]]></category><category><![CDATA[Domain Modeling]]></category><category><![CDATA[Software Design]]></category><category><![CDATA[Local First]]></category>
                    <description><![CDATA[The toggle looked trivial.
But behind a single button, sessions, state transitions, and domain policies were all entangled.
This is the story of how the simplest feature exposed a broken design.]]></description>
                    <content:encoded><![CDATA[<h2 id="the-problem-this-article-addresses-%E2%80%94-why-the-simplest-looking-feature-was-actually-dangerous">The Problem This Article Addresses — Why the Simplest-Looking Feature Was Actually Dangerous</h2><p>Once you reorganize the domain structure, it is very easy to feel a sense of relief. When previously tangled concepts are separated by responsibility and each object clearly defines what it owns, it starts to feel like the hard part is over. From that point on, it seems like all that remains is to layer functionality on top. And in many ways, it does look that way. ReadingRecord manages state, ReadingSession handles time and metrics, and TimelineEvent records facts. Responsibilities are separated, and boundaries are clearly drawn. At this stage, the design appears stable. It feels like a complex puzzle has finally come together.</p><p>But there is something easy to overlook at this moment. The structure may be clean, but what actually <em>happens</em> within that structure has not yet been validated. This article begins exactly at that point. The structure looked correct, but as soon as real behavior was introduced, unexpected complexity surfaced. The first collision point was toggleSession. On the surface, this is hardly even worth calling a “feature.” One button, one condition, one branch. It simply alternates between “start reading” and “stop reading.” In most cases, something like this sits at the periphery of a system rather than at its core. And that is precisely why it is dangerous. The moment we decide something is not important, we stop verifying the complexity hidden inside it.</p><p>The problem was that toggleSession was not just a simple UI interaction. This button was not merely changing a state; it was acting as an entry point that passed through the entire domain. From the user’s perspective, it is just a button press, but behind that action, session creation and termination, progress updates, event recording, and state transitions are all chained together. In other words, this button is effectively the “gateway” to the system. The policies executed at this gateway determine the consistency of the entire dataset. This is where the first real question emerges. Can this really be handled in a simple way?</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-107.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-107.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-107.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-107.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>This question is not just a minor doubt; it becomes the starting point for every design revision that follows. Up until now, the focus was on organizing structure. From this point forward, the focus shifts to validating how that structure actually behaves. And what emerges through this process is a story about how easily the assumptions we believed to be “simple” can collapse.</p><h2 id="the-initial-assumption-%E2%80%94-a-toggle-can-be-handled-with-a-single-if-else">The Initial Assumption — A Toggle Can Be Handled with a Single if-else</h2><p>When toggleSession was first conceived, the thinking was surprisingly straightforward. If an activeSession exists, end it. If not, start one. It felt like it could not get any simpler than that. In fact, this pattern is commonly used in timer-based or session-based features. Because of that familiarity, the assumption was accepted without question. Coming right after a complex domain restructuring, there was an even stronger expectation that at least a small feature like this should be easy to implement.</p><p>The mental model at that time could be summarized in a single piece of code:</p><pre><code class="language-plaintext">if (activeSession exists)
    endSession()
else
    startSession()
</code></pre><p>This structure feels completely natural. If there is a state, terminate it; if not, initiate it. The condition is clear, and the branching is simple. More importantly, it feels like there is nothing left to think about. At that moment, this simplicity was interpreted as proof that the design was solid. Because the domain had been broken down cleanly, each function could now be expressed in a minimal and elegant way. In other words, this code appeared to validate the design itself.</p><p>But in reality, the opposite was true. This code was not the result of solving the problem; it was a state in which the problem was being hidden. Trying to explain every situation with a single condition—activeSession—was an attempt to deliberately ignore the various states and edge cases that actually existed. When we see a simple interface, we often assume the underlying system is simple as well. In this case, however, the simplicity of the interface was masking the complexity underneath.</p><p>The danger of this assumption lies in how easily it is accepted. The shorter and clearer the code appears, the less likely we are to question it. But when the domain itself is complex, it is almost impossible to handle every situation with a single condition. At this stage, that reality was not yet obvious. But as soon as implementation began, this assumption started to break down rapidly. And that breakdown did not happen all at once—it unfolded gradually, as small questions began to accumulate.</p><h2 id="the-beginning-of-the-collapse-%E2%80%94-the-moment-questions-start-attaching">The Beginning of the Collapse — The Moment Questions Start Attaching</h2><p>The moment actual implementation began, unexpected questions started to appear one by one. At first, they seemed trivial. Where should progress be input when ending a session? What should happen if progress is null? What if the user presses the button twice? Are session termination and completion the same event? What happens if the app shuts down right before a session ends? Each of these questions, in isolation, looks like a small and manageable issue. It is easy to think, “we can deal with that later.” But the real problem is that these questions do not remain isolated—they begin to connect.</p><p>Trying to resolve one question inevitably creates another. For example, if progress is made optional, then null states must be handled. Allowing null makes it unclear how to determine completion. Attempting to infer completion automatically changes when events should be created. Changing event timing introduces duplication issues. In this way, a single decision starts forcing additional problems. At this point, toggleSession can no longer be maintained as a simple conditional branch.</p><p>A critical shift happens here. The nature of the problem changes. Initially, this was thought of as a “button interaction” issue. But as questions accumulate, it becomes clear that this is not a UI-level problem—it is a domain policy problem. The button is merely a trigger; what truly matters is the set of rules executed behind it. In other words, toggleSession is no longer just a feature—it becomes a policy execution point. The moment this realization occurs, the original if-else structure is no longer valid.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-108.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-108.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-108.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-108.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>By this stage, the collapse has already begun. The code itself has not completely broken yet, but the assumptions that support it are no longer holding. What matters here is that this collapse is not caused by bugs, but by insufficient assumptions. What was believed to be a simple problem actually contained complex policies. And that truth only reveals itself once implementation begins. From this point forward, the approach must change. Instead of forcing this complexity to remain hidden, it must be acknowledged and redefined structurally.</p><h2 id="the-core-issue-%E2%80%94-toggle-is-not-a-state-change-but-policy-execution">The Core Issue — Toggle Is Not a State Change, but Policy Execution</h2><p>Once questions began to accumulate, it became impossible to view this problem from the original perspective. At first, it seemed like a simple state transition, but in reality, it carried far more meaning. The existence of <code>activeSession</code> was not just a condition; it was a compressed representation of the system’s current state. And what action should be taken from that state was not a simple branch, but a process of executing multiple predefined policies. In other words, <code>toggleSession</code> was not a button that changes state, but an entry point through which domain rules are executed.</p><p>Understanding this distinction is critical. State changes focus on outcomes. What value changed, which field was updated—these are the primary concerns. Policy execution, however, focuses on the process. What conditions were validated, in what order things were handled, and which rules were applied—these matter more. This difference is reflected directly in the structure of the code. Code centered on state changes is short and simple. But code centered on policy execution becomes longer, introduces stages, and each stage carries meaning.</p><p>At this point, <code>toggleSession</code> needs to be redefined. It is no longer a function that simply toggles between start and end. It is a <strong>flow that executes appropriate domain procedures based on the current state</strong>. This is not just a change in wording—it fundamentally shifts the direction of the design. Instead of explaining what the button does, we now have to explain which policies it executes. And those policies cannot be reduced to a single condition.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-109.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-109.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-109.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-109.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>With this shift in perspective, problems that were previously invisible begin to make sense. Why null handling was necessary, why duplicate events could occur, why the termination logic became complex—all of these connect into a single narrative. In the end, the issue was not the code itself, but how we were interpreting it. And from this point, we move to the next step: making the policy execution concrete.</p><h2 id="the-explosion-of-session-termination-%E2%80%94-when-a-simple-action-becomes-a-procedure">The Explosion of Session Termination — When a Simple Action Becomes a Procedure</h2><p>This shift becomes most apparent in the session termination logic. Starting a session is relatively simple. You create a new session, record the start time, and log the necessary event. This process can be expressed as a mostly linear flow. But termination is entirely different. Ending a session is not just about filling in the <code>endedAt</code> field. It is a branching point where multiple states are resolved simultaneously and new states are derived as a result.</p><p>When actually implementing the termination logic, it becomes clear that far more steps are involved than expected. First, input must be received and validated. Then the session must be updated, events must be created, and depending on conditions, the record state may need to change. All of this must happen within a single transaction. If any part is missing or executed out of order, data consistency can break. In other words, termination is not a simple action—it is a procedure composed of multiple policies.</p><p>This complexity becomes especially evident in the problem of idempotency. Users pressing a button twice is extremely common. Due to network latency, UI delays, or asynchronous processing, the same request can be triggered multiple times. What happens if the termination logic runs twice? The same event could be created twice, the state could be updated twice, and statistical data could become corrupted. This cannot be solved with a simple “prevent duplicates” condition. The entire procedure must be designed so that even if executed multiple times, the result remains consistent.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-110.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-110.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-110.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-110.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>At this point, one fact becomes undeniable. The “toggle” we initially imagined does not actually exist. What exists are two entirely different procedures: start and end, each with completely different levels of complexity. The concept of a toggle was merely a simplification used to group these two together. And that simplification was hiding the real problem. Now, that simplification must be abandoned, and each procedure must be exposed as it truly is.</p><h2 id="not-a-fix-but-a-redefinition-%E2%80%94-turning-toggle-into-a-flow">Not a Fix, but a Redefinition — Turning Toggle into a Flow</h2><p>At this stage, what is needed is not to solve the problem, but to redefine it. Trying to keep the existing if-else structure and adding more conditions is no longer viable. That approach does not solve the problem—it only buries it deeper. Instead, the structure itself must be discarded. This is not about modifying code while preserving the concept of a toggle; it is about reinterpreting what “toggle” means.</p><p>This is where the flow-based structure emerges. Now, <code>toggleSession</code> is no longer a single function, but two clearly defined flows: a start flow and an end flow. These are not simple branches, but independent procedures with their own structure. The existence of <code>activeSession</code> no longer determines “what to do,” but rather “which flow to execute.” This shift changes not only the code structure, but the entire way of thinking.</p><pre><code class="language-plaintext">check activeSession
→ start flow OR end flow
→ validate inputs
→ execute domain policies
→ create events
→ update record
</code></pre><p>What matters in this structure is that each stage carries clear meaning. Validation is not just input checking, but ensuring the preconditions of the policy. Event creation is not just logging, but recording state transitions within the system. And all of these stages are not isolated—they are connected within a single flow. That connection is what creates stability in the system.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-111.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-111.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-111.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-111.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>Now, <code>toggleSession</code> is no longer a simple button interaction. It is an entry point for executing domain policies, where multiple flows are selected and executed. Only through this redefinition can all the problems that emerged earlier be handled in a consistent way. And at this point, the domain separation defined in the previous section finally begins to reveal its true value.</p><h2 id="the-moment-domain-separation-actually-starts-working">The Moment Domain Separation Actually Starts Working</h2><p>Once toggle was redefined as a flow, one fact finally became visible. The domain separation carried out in the previous stage was not just structural cleanup. At the time, it seemed as though the main purpose was simply to divide responsibilities. The distinction that ReadingRecord manages state, ReadingSession handles time and numeric values, and TimelineEvent records facts felt almost like a design principle. But whether that design truly had meaning could not be known until actual behavior was placed on top of it. The process of reconstructing toggleSession as a flow was exactly that validation stage.</p><p>What appeared first in that process was the boundary of responsibility. As the termination logic took shape, it became clearer where each piece of data had to live. Progress should exist only in the session, completion state should be managed only by the record, and events should be created only in the event layer. The clearer this separation became, the more naturally each step also became organized in terms of what it had to do. If those boundaries were blurred, the same data would end up being managed in multiple places, or different layers would begin to share the same responsibility. The moment that happens, consistency starts to collapse.</p><p>The important point here is that this structure does not exist for the sake of “cleanliness.” We often explain the reason for splitting layers or separating objects in terms of readability or maintainability. But in this case, there is a more fundamental reason. It is a structure for <strong>preserving consistency</strong>. Only when each responsibility is clearly divided can a complex policy be executed across multiple steps while still keeping the result consistent. If progress had existed both in Record and in Session, it would have become unclear which value should be used as the basis for determining completion. That kind of ambiguity eventually turns into a bug.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-112.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-112.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-112.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-112.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>At this point, domain separation is no longer an abstract design principle. It becomes a necessary condition for keeping actual behavior stable. And because of this structure, even while constructing a complex flow, it becomes possible to avoid confusing the responsibility of each stage. Until then, structure had been theory. Now that structure was beginning to function as a tool that solved real problems.</p><h2 id="it-is-not-code-but-policy-%E2%80%94-the-structure-revealed-through-pseudocode">It Is Not Code but Policy — The Structure Revealed Through Pseudocode</h2><p>After the structure was redefined on a flow basis, an interesting shift appeared in the process of translating it into code. Code began to look less like simple logic and more like a means of expressing policy. Previously, I had believed that the shorter the code, the better. I thought good code meant implementing a feature with concise conditionals and the fewest possible branches. But at this stage, that standard was no longer valid. On the contrary, code that made each stage explicit became the better code.</p><p>For example, if the termination logic is expressed as pseudocode, it takes a form like this.</p><pre><code class="language-plaintext">function endSession(recordId, inputProgress):
    record = findRecord(recordId)

    if (record.activeSessionId == null)
        return

    session = findSession(record.activeSessionId)

    if (session.endedAtUtc != null)
        return

    validate(inputProgress)

    session.endedAtUtc = nowUtc()
    session.progressPct = inputProgress
    save(session)

    createEvent(SESSION_ENDED)

    if (inputProgress == 100):
        record.status = COMPLETED
        createEvent(RECORD_COMPLETED)

    record.activeSessionId = null
    save(record)
</code></pre><p>On the surface, this code does not look especially complicated. But what matters is the meaning embedded inside it. Each stage does not merely represent a processing step. It represents a policy. validate is not simply checking an input value; it is confirming whether a domain rule has been satisfied. createEvent is not just leaving a log; it is an act of exposing the system’s state change outward. And the part that updates the record is not merely a field mutation; it is the process of redefining the current state of the system.</p><p>Seen in this way, the length of the code no longer matters. What matters is how clearly the code expresses policy. Short code is not always good code. In fact, code that hides policy is more dangerous. At this stage, the role of code itself gets redefined. Code is not a tool for implementing a feature. It is closer to a <strong>document that makes domain policy explicit</strong>.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-113.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-113.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-113.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-113.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>This change in perspective affects every implementation choice that comes afterward. No longer is the goal to reduce code at all costs. Instead, the structure is divided so that policy is clearly visible. And as a result, it becomes possible to trace the behavior of the entire system directly through the code itself.</p><h2 id="it-was-not-the-design-but-the-thinking-that-changed">It Was Not the Design but the Thinking That Changed</h2><p>After going through all of this, one change remains that is more important than the visible changes on the surface. It is not the structure of the code, but the change in the way of thinking. At first, toggleSession was seen as a simple feature, and the concern was how to implement that feature. But now the questions become completely different. What policy does this action execute, in what order should that policy be applied, and what kind of problem occurs if that flow breaks. These become the first questions.</p><p>This change is not simply a matter of writing better code. It changes the way the problem itself is seen. Previously, the thinking was in units of features. Now it shifts into thinking in units of flows and policies. Because of that, even when looking at the same problem, the approach becomes completely different. For example, when addressing duplicate execution, the focus is no longer on just adding one more conditional. The goal becomes designing the entire flow so that the result does not change even if the same execution is repeated. That difference has a major effect on the stability of the system.</p><p>In the course of this process, several choices changed. Values that had been left nullable were removed. Event creation was moved from the UI layer up into the domain layer. activeSession stopped being found implicitly through queries and instead began to be managed explicitly. Looked at one by one, these may seem like small modifications. But they all move in the same direction. <strong>Making policy visible, and shaping the flow into something controllable.</strong></p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-114.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-114.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-114.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-114.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>At this point, the domain is no longer in an unstable state. It would be too much to say it has become perfect, but at least when a problem occurs, it is now clear where to look. And that clarity is what creates stability in the design. This piece is not simply a record of implementing toggle logic. It is the process of showing how a way of thinking changed through one feature. And that change leads directly into the next stage. Now the question becomes how another axis, the axis of events, should be defined on top of this structure.</p><h2 id="the-position-of-this-article-%E2%80%94-the-first-record-of-%E2%80%9Cexecution-collapse%E2%80%9D">The Position of This Article — The First Record of “Execution Collapse”</h2><p>If you follow the flow up to this point, a clear turning point emerges. In the previous article, the focus was on organizing the domain structure. Responsibilities were separated across objects, state and records were divided, and the overall structure was shaped into something stable. That process was about design, and it belonged to a relatively static domain. However, what is covered in this article is fundamentally different in nature. Here, we deal with what happens the moment actual behavior is placed on top of that structure. And the result was not what was expected. The structure was correct, but the way it operated was anything but simple.</p><p>This article exists precisely at that point. This is not about structure, but <strong>the first case that records the moment when execution collapses</strong>. On the surface, toggleSession looked like one of the simplest features. But in reality, it was a flow that penetrated the entire domain. That is why, in the process of implementing this single function, every design decision that had been previously defined was put to the test. And what emerged from that process was not whether the design was right or wrong, but how that design was executed. In other words, structure alone was not enough, and it became necessary to define how behavior should actually flow on top of that structure.</p><p>At this point, another important shift appears. The collapses up until now were conceptual. Problems such as the session model not fitting, or the limitations of a state-based approach, belonged to the level of ideas. But this collapse is far more concrete. The moment actual code is about to be written, it becomes clear that the structure cannot sustain that code. This difference matters. Conceptual collapse can be resolved by adjusting direction, but execution collapse cannot be resolved unless the structure itself is redefined. That is why this article is not just a record of implementation, but a case that shows how design collapses again in the process of being executed.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-115.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-115.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-115.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-115.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>The role of this article within the series is clear. If #2 was about reorganizing structure, this article is about exposing what happens when that structure actually runs. And through this process, a new criterion emerges. Design cannot be evaluated by structure alone—it must be validated through execution. This criterion affects every article that follows. We no longer ask, “Is this structure correct?” Instead, we begin by asking, “How does this structure actually behave in execution?”</p><p>This flow naturally leads to the next step. The problems revealed through toggleSession ultimately connect to how events are handled. The timing of when events are created, and what data those events carry, directly shapes the overall flow. That is why the next article will focus on how TimelineEvent was defined, and why there was an intentional decision to keep events as thin as possible. If everything so far was about the collapse of execution, then the next step becomes the process of establishing a new standard to stabilize that execution.</p>]]></content:encoded>
                </item>
                <item>
                    <title>When Linux I/O Is Decided — Execution-Time Model of stdin, stdout, pipe, and redirection</title>
                    <link>https://en-signal.ceak.dev/linux-io-execution-model-stdin-stdout-pipe-redirection/</link>
                    <guid isPermaLink="true">https://en-signal.ceak.dev/linux-io-execution-model-stdin-stdout-pipe-redirection/</guid>
                    <pubDate>Sat, 11 Apr 2026 16:28:18 +0900</pubDate>
                    <dc:creator><![CDATA[ceak]]></dc:creator>
                    <category><![CDATA[🎯Docs]]></category><category><![CDATA[📚 Linux I/O and Data Flow — From Streams to Pipelines]]></category><category><![CDATA[Linux]]></category><category><![CDATA[Shell]]></category><category><![CDATA[io stream]]></category>
                    <description><![CDATA[Linux I/O is not a flow but a pre-execution state.
This article explains how stdin, stdout, pipes, and redirection are actually wired before a process starts.]]></description>
                    <content:encoded><![CDATA[<h2 id="why-does-the-same-command-produce-different-results">Why does the same command produce different results?</h2><p>When executing commands in a terminal, many people assume that if the command itself is the same, the result should also be fundamentally the same. This assumption does not cause major issues for simple commands. However, it quickly breaks down once redirection and error output are involved. For example, <code>command &gt; out.txt 2&gt;&amp;1</code> and <code>command 2&gt;&amp;1 &gt; out.txt</code> look very similar on the surface. Both appear to intend to send stdout and stderr to the same place. But in reality, the first command sends both normal output and error output to the file, while the second command may leave stderr in the terminal and only send stdout to the file. The important point here is that the difference in results is not caused by the arrangement of characters, but by the <strong>order</strong> in which the shell interprets and applies the statement.</p><p>If this difference is not verified directly, the explanation tends to remain abstract. You can observe the difference immediately with a simple test. Force stderr to occur by attempting to read a non-existent file, while also producing stdout at the same time.</p><pre><code class="language-bash">echo "stdout"; cat not_exist_file
</code></pre><p>Run this command in the following two ways.</p><pre><code class="language-bash">echo "stdout"; cat not_exist_file &gt; out1.txt 2&gt;&amp;1
echo "stdout"; cat not_exist_file 2&gt;&amp;1 &gt; out2.txt
</code></pre><p>In the first case, both normal output and error messages are written to <code>out1.txt</code>. In the second case, only normal output is written to <code>out2.txt</code>, and the error message remains printed in the terminal. This difference can be directly observed, and through the result, you can verify that “even statements that look the same in meaning are processed differently internally.” What matters here is not memorizing the result, but recognizing that there is an internal process that produces this result.</p><p>When encountering this difference for the first time, many explanations drift toward memorizing syntax. Some people tell you to memorize <code>2&gt;&amp;1</code>. Others suggest using a certain order as a habit. However, this approach quickly reveals its limitations when conditions change slightly. The reason is that what is needed here is not memorization of patterns, but a structural understanding of what state changes occur just before execution. On the screen, all output simply looks like text. But inside the system, stdout and stderr are not the same from the beginning. And the shell reconnects these paths one by one before executing the command.</p><p>Therefore, even if commands look the same externally, the final result changes depending on which connections were established first internally. At this point, the question naturally shifts. It is no longer “which syntax should be used,” but “what does the shell change, and how, before execution?”</p><p>▶ To understand this concept more deeply:&nbsp;<a href="https://en-signal.ceak.dev/linux-dup-dup2-redirection-order-2and1/" rel="noreferrer">Understanding dup and dup2 — The Real Reason 2&gt;&amp;1 and Redirection Order Behave Differently</a></p><p>To move to the next step, this question must be clearly defined. Once you begin to look for the cause of result differences not in surface syntax but in the system’s preparation process, several assumptions that were previously taken for granted begin to emerge. In the next section, we need to start by identifying exactly what those incorrect assumptions are.</p><h2 id="what-we-are-assuming-incorrectly">What we are assuming incorrectly</h2><p>The most common misunderstanding when learning Linux I/O is the belief that stdout, stderr, and pipes exist as fixed channels from the beginning. When you type a command into the terminal, you see output. So stdout feels like it naturally goes to the terminal. When an error occurs, you see a message. So stderr is easily understood as just an additional output channel on the screen. When pipes and redirection are added, people tend to interpret them as “temporarily changing an existing path.” This interpretation is convenient at the beginner level. However, it is not accurate at the system level. More precisely, it is merely an explanation attached after observing the result.</p><p>This misunderstanding can be immediately broken by checking the actual state through <code>/proc</code>. To see what input/output targets the current shell process has, you can use the following command.</p><pre><code class="language-bash">ls -l /proc/$$/fd
</code></pre><p>Here, <code>$$</code> represents the PID of the current shell process. The output shows what fd 0, 1, and 2 are pointing to. In the default state, they all point to the terminal device. However, if you apply redirection and run the same command, the result changes.</p><pre><code class="language-bash">bash -c 'ls -l /proc/$$/fd' &gt; out.txt
</code></pre><p>In this case, fd 1 no longer points to the terminal but to a file. In other words, stdout is not “originally going to the terminal,” but was simply set that way in the default state. Using pipes changes the fd connections again in a similar manner.</p><p>The key takeaway from this experiment is simple. Output results are always a post-hoc phenomenon, and what truly matters is the file descriptor state a process has when it starts. stdout is not conceptually “the screen.” stderr is not “a separate screen for errors.” A pipe is not a pre-existing channel between programs. The shell reads the command and then, just before execution, determines one by one what each file descriptor will point to.</p><p>From this perspective, <code>&gt;</code> is not a function that “sends text to a file.” It is an operation that changes the target of stdout to a file. Similarly, <code>|</code> is more accurately understood not as “output flowing into the next command,” but as reconnecting the stdout of the previous process and the stdin of the next process through a specific kernel object. Missing this point makes it impossible to predict new situations. On the other hand, once this assumption is discarded, command syntax begins to be read not as a description of results, but as an expression of state changes.</p><p>▶ To understand this concept more deeply:&nbsp;<a href="https://en-signal.ceak.dev/linux-shell-execution-structure-complete-guide/" rel="noreferrer">Complete Guide to Linux Shell Execution — How Programs Are Executed and Controlled</a></p><p>Now the <strong>frame of reference</strong> has changed. Instead of looking at output results, you must look at what state is created before execution. In the next step, we need to examine in more detail how this “pre-execution state” is actually constructed.</p><h2 id="nothing-is-connected-before-execution">Nothing is connected before execution</h2><p>The statement “nothing is connected before execution” does not literally mean a completely empty state. More precisely, it means that the I/O connection structure that the user will ultimately observe has not yet been finalized. Consider entering a command like <code>grep foo file.txt | sort &gt; out.txt 2&gt;&amp;1</code>. At this point, only a single string exists. However, for the program to actually run, several connection steps must occur beforehand. A pipe must be created. The stdin and stdout of each process must be connected to the ends of that pipe. The stdout of the final process must be redirected to a file. stderr may also be reassigned to reference the current target of stdout.</p><p>To observe this process directly, you can use <code>strace</code>. Running the following command allows you to see which system calls the shell uses internally.</p><pre><code class="language-bash">strace -f -e trace=process,dup2,pipe,open bash -c 'grep foo file.txt | sort &gt; out.txt 2&gt;&amp;1'
</code></pre><p>The output shows calls such as <code>pipe</code>, <code>dup2</code>, <code>open</code>, and <code>execve</code> in sequence. What can be confirmed here is straightforward. Files are opened, pipes are created, and file descriptor reassignment is performed before the program is executed. In other words, the single line of command you entered goes through multiple preparation steps internally before execution. This stage is exactly the “pre-execution state construction.”</p><p>This stage is important because the program itself is completely unaware of it. <code>grep</code> does not know whether it is placed before a pipe. <code>sort</code> does not know whether its output is redirected to a file. Programs simply read and write through fd 0, 1, and 2. What is connected behind those file descriptors must already be determined before execution. The shell first opens the necessary files, creates the required pipes, and applies the necessary fd reassignments. Only then does it execute the program.</p><p>Understanding this structure also explains why redirection and pipes naturally work together. They may appear as different features, but in reality, they are different ways of solving the same problem: “what should each file descriptor point to before execution?” File redirection makes a file descriptor point to a file. A pipe makes a file descriptor point to a pipe object. <code>2&gt;&amp;1</code> makes fd 2 reference the current target of fd 1.</p><p>At this level, I/O is no longer an abstract flow. It is a state model constructed just before execution. And the smallest unit that composes this model is the file descriptor. Therefore, in the next step, file descriptors must be understood not as simple numbers, but as references representing actual connection relationships.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-120.png" class="kg-image" alt="" loading="lazy" width="1024" height="559" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-120.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-120.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-120.png 1024w" sizes="(min-width: 720px) 720px"></figure><h2 id="a-file-descriptor-is-not-a-%E2%80%9Cvalue%E2%80%9D-but-a-reference">A file descriptor is not a “value,” but a <strong>reference</strong></h2><p>From this point on, if you understand file descriptors as simple numbers, the explanation cannot progress any further. The numbers 0, 1, and 2 are convenient notation at the beginner level. However, to explain the structure at execution time, these numbers must be understood not as “labels of output types,” but as <strong>handles that indicate which I/O target a process is currently connected to</strong>. In reality, a file descriptor is an integer used by a process to identify open I/O objects such as files, terminals, and pipes, and stdin, stdout, and stderr are also represented as FD 0, 1, and 2 respectively. The important point is that the number itself does not contain any data. The number only expresses the fact that it is connected somewhere. Therefore, redirection is not a function that moves output strings elsewhere, but an operation that changes the target that the number points to.</p><p>This concept can be directly verified through <code>/proc</code>. To see what the file descriptors of the current process are pointing to, run the following command.</p><pre><code class="language-bash">ls -l /proc/$$/fd
</code></pre><p>Here, fd 1 points to the terminal by default. Now, if you check the same thing after changing stdout to a file, the difference becomes apparent.</p><pre><code class="language-bash">bash -c 'ls -l /proc/$$/fd' &gt; out.txt
</code></pre><p>In this case, fd 1 no longer points to the terminal but to the file <code>out.txt</code>. The output did not “move” to the file; rather, the program was executed in a state where fd 1 was already set to point to the file. This distinction is critical. Because from this point on, all redirection behavior is explained not as “data movement,” but as <strong>reference reassignment</strong>.</p><p>At this point, it is especially important to recognize that explaining <code>2&gt;&amp;1</code> as “sending stderr to stdout” misses the core. A more accurate explanation is not that stderr changes into the value of stdout, but that <strong>stderr is reassigned to reference the same target that stdout is currently pointing to</strong>. To verify this distinction, run the following experiment.</p><pre><code class="language-bash">bash -c 'ls -l /proc/$$/fd' 2&gt;&amp;1
</code></pre><p>In this case, fd 2 ends up pointing to the same target as fd 1. In the <code>/proc</code> output, you can confirm that both FDs point to the same inode. This result shows that stderr does not “follow” stdout dynamically, but instead acquires a fixed reference to the stdout target at a specific point in time.</p><p>If you do not distinguish between “value copying” and “reference sharing,” you can never explain ordering issues. This is also why <code>dup</code> and <code>dup2</code> are important. These system calls do not copy file contents; they rearrange file descriptors so that one descriptor points to the same I/O target as another. The program still writes to FD 1 as before. What has changed is the reference structure created outside the program, specifically by the shell just before execution.</p><p>▶ To understand this concept more deeply:&nbsp;<a href="https://en-signal.ceak.dev/linux-file-descriptor-dup-dup2-redirection-internals/" rel="noreferrer">Linux File Descriptors (fd), dup, and dup2 — The Real Structure Connecting Execution and Redirection</a></p><p>Now there is one clear <strong>frame of reference</strong>. A file descriptor is not a value, but a reference. Therefore, in the next step, what matters more than “what was connected” is <strong>when that connection was established</strong>.</p><h2 id="a-single-order-changes-all-results">A single order changes all results</h2><p>If you have properly understood redirection in the Linux shell, you can now move to the most commonly confusing point. Why do <code>command &gt; file 2&gt;&amp;1</code> and <code>command 2&gt;&amp;1 &gt; file</code> produce different results? Many explanations stop at “the first command is correct” or “just memorize this order.” But this is not a matter of syntax preference. The shell does not interpret redirection all at once. After parsing the command, just before execution, the shell <strong>applies file descriptor reconfiguration sequentially</strong>. Therefore, which target is changed first determines the meaning of subsequent operations.</p><p>This difference can be directly observed. Run the following test.</p><pre><code class="language-bash">bash -c 'echo stdout; echo stderr &gt;&amp;2' &gt; out1.txt 2&gt;&amp;1
</code></pre><pre><code class="language-bash">bash -c 'echo stdout; echo stderr &gt;&amp;2' 2&gt;&amp;1 &gt; out2.txt
</code></pre><p>In the first case, both stdout and stderr are written to <code>out1.txt</code>. In the second case, only stdout is written to <code>out2.txt</code>, while stderr is printed to the terminal. This result is directly observable and demonstrates that this is not a matter of memorized rules, but an actual behavioral difference.</p><p>Now this result must be interpreted structurally. In the first command, <code>&gt; out1.txt</code> is applied first, so stdout is set to point to the file. Then <code>2&gt;&amp;1</code> is applied, causing stderr to reference the <strong>current stdout target</strong>, which is the file. In contrast, in the second command, <code>2&gt;&amp;1</code> is applied first. At that moment, stdout still points to the terminal. Therefore, stderr also points to the terminal. After that, <code>&gt; out2.txt</code> is applied, changing only stdout to the file while stderr remains pointing to the original target.</p><p>This structure can also be verified with <code>strace</code>.</p><pre><code class="language-bash">strace -e dup2 bash -c 'echo test &gt; out.txt 2&gt;&amp;1'
</code></pre><p>The output shows the order of <code>dup2</code> calls. It records sequentially which FD is changed to which target. From this log, you can confirm that redirection is not declarative but procedural in its application.</p><p>Ultimately, redirection syntax is not decorative text; it is a syntax through which the shell <strong>redraws the I/O wiring sequentially</strong>. Once this is understood, the result is no longer accidental. If you know what stdout points to at a given moment, you can determine the meaning of the subsequent <code>2&gt;&amp;1</code>.</p><p>▶ To understand this concept more deeply:&nbsp;<a href="https://en-signal.ceak.dev/linux-dup-dup2-redirection-order-2and1/" rel="noreferrer">Understanding dup and dup2 — The Real Reason 2&gt;&amp;1 and Redirection Order Behave Differently</a></p><p>At this point, redirection begins to look completely different. In the next step, the same principle must be applied to pipes. Pipes, too, are not results, but connection structures created before execution.</p><h2 id="a-pipe-is-not-a-flow-but-a-connection-operation">A pipe is not a flow, but a <strong>connection operation</strong></h2><p>Now pipes must be reconsidered. Many explanations summarize <code>|</code> as “passing the output of the previous command to the next command.” This explanation is not wrong at the usage level. However, it is insufficient to explain the execution-time structure. To understand pipes, they must be viewed not as data flow, but as <strong>an operation in which the shell connects the file descriptors of two processes to a specific kernel object</strong>.</p><p>This structure can be directly observed through <code>strace</code>. Run the following command.</p><pre><code class="language-bash">strace -f -e pipe,dup2,execve bash -c 'echo test | cat'
</code></pre><p>The output first shows a <code>pipe()</code> call. Then two processes are created, and in each process, <code>dup2</code> calls connect fd 1 and fd 0 to the two ends of the pipe. Finally, <code>execve</code> is called to execute the actual program. From this sequence, it becomes clear that the pipe is not created during execution, but is fully prepared before execution begins.</p><p>To verify this state more directly, you can use <code>/proc</code>. After running a process that uses a pipe, if you check the file descriptors of that process, you can see that certain FDs are displayed in the form <code>pipe:[inode]</code>.</p><pre><code class="language-bash">ls -l /proc/&lt;pid&gt;/fd
</code></pre><p>Here, you can confirm that stdout or stdin points not to a file, but to a pipe object. In other words, a pipe is not an abstract flow, but a real object managed by the kernel, and file descriptors reference that object.</p><p>Once this perspective is established, it becomes clear that redirection and pipes exist at the same conceptual level. File redirection connects stdout to a file. <code>2&gt;&amp;1</code> connects stderr to the current target of stdout. A pipe connects the stdout of one process to the stdin of another process via the two ends of a pipe object. They appear as different features, but in reality, they are different answers to the same question: “What do fd 0, 1, and 2 point to when this process starts?”</p><p>▶ To understand this concept more deeply:&nbsp;<a href="https://en-signal.ceak.dev/linux-pipe-command-explained-with-examples/" rel="noreferrer">What Is a Pipe (|)? A Clear Guide to the Linux Pipe Concept</a></p><p>At this stage, I/O no longer appears as a flow. It is a connection structure constructed before execution. In the next step, it becomes necessary to clearly define what the stdout, stderr, and pipe results we actually observe represent—that is, why I/O must be understood not as structure, but as a product of execution timing.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-121.png" class="kg-image" alt="" loading="lazy" width="1024" height="559" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-121.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-121.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-121.png 1024w" sizes="(min-width: 720px) 720px"></figure><h2 id="what-we-see-as-io-is-only-the-result">What we see as I/O is only the result</h2><p>If you examine file descriptors, redirection order, and pipe connections together, a single common conclusion emerges. The output that users see in the terminal is not the essence of I/O. It is merely the result of the connection structure that was constructed just before execution being exposed externally. If this distinction is not clearly understood, people will continue to build concepts based on observed phenomena. If something appears on the screen, they assume it is stdout. If it is written to a file, they assume redirection occurred. If the next command reads it, they assume a pipe was connected. These explanations may be sufficient to describe results. However, concepts built on results collapse immediately when encountering new situations. They fail to explain why the same command produces entirely different outputs in different execution environments.</p><p>This difference becomes much clearer when directly compared. If you run the following commands, the same program appears to behave in completely different ways.</p><pre><code class="language-bash">python3 -c 'import sys; print("out"); print("err", file=sys.stderr)'
</code></pre><pre><code class="language-bash">python3 -c 'import sys; print("out"); print("err", file=sys.stderr)' &gt; out.txt
</code></pre><pre><code class="language-bash">python3 -c 'import sys; print("out"); print("err", file=sys.stderr)' 2&gt; err.txt
</code></pre><pre><code class="language-bash">python3 -c 'import sys; print("out"); print("err", file=sys.stderr)' | cat
</code></pre><p>In the first case, both stdout and stderr appear in the terminal. In the second case, only stdout is saved to a file while stderr remains in the terminal. In the third case, only stderr is redirected to a file while stdout remains in the terminal. In the fourth case, stdout is passed through a pipe to <code>cat</code>, while stderr still remains in the terminal. The critical point here is that the program code has not changed at all. The only thing that changed is the fd connection state prepared by the shell just before execution. This experiment breaks the assumption that “visible output is an inherent property of the program.” The destination of output is not a direct property of the program’s internal logic, but a result determined by how the execution environment was prepared.</p><p>More importantly, from the program’s perspective, the screen, file, and pipe are not direct concepts at all. The program simply reads from fd 0 and writes to fd 1 or fd 2. What the user calls “output appearing on the screen” is simply the result of fd 1 pointing to the terminal at that moment. What the user calls “saved to a file” is simply the result of fd 1 having been changed to point to a file beforehand. What the user calls “passing to the next command” is simply the result of fd 1 and another process’s fd 0 being connected to the two ends of a pipe object. From this perspective, I/O is not a completed behavior inside the program. It is one aspect of the execution environment jointly constructed by the shell, kernel, and process.</p><p>Once this perspective is accepted, the user can now infer the structure by looking at the output result. It becomes possible to determine why a message went into a file, why it remained on the screen, or why it did not pass to the next pipeline stage, based solely on the result. Once this capability is developed, the next question naturally shifts to practice. In real scripts and operational environments, the issue becomes how this structure should be handled based on clear criteria.</p><p>▶ To understand this concept more deeply:&nbsp;<a href="https://en-signal.ceak.dev/linux-io-streams-stdin-stdout-pipe-redirection-explained/" rel="noreferrer">Understanding Linux I/O and Streams — stdin, stdout, pipes, and redirection explained</a></p><h2 id="in-practice-%E2%80%9Chow-to-execute%E2%80%9D-matters-more-than-%E2%80%9Cwhere-to-output%E2%80%9D">In practice, “how to execute” matters more than “where to output”</h2><p>The most common mistake when handling I/O in practice is composing commands based only on output results. If you want to store logs in a file, you append <code>&gt; logfile</code>. If you also want to store errors, you add <code>2&gt;&amp;1</code>. If you want to run quietly, you send output to <code>/dev/null</code>. This approach appears sufficient on the surface. However, as conditions become more complex—such as in operational scripts, batch jobs, or CI environments—this result-driven composition becomes unstable. Some outputs are written to files, while others remain in the terminal. Some commands pass through pipes, while certain errors are not visible to subsequent stages. The root cause of this problem is not insufficient memorization of command syntax. The core issue is the lack of awareness of what fd state a process inherits before execution.</p><p>This difference becomes immediately apparent when downstream processing is involved. For example, when stdout is used as input to the next command, stdout must be preserved as a data channel. If stderr is mixed into it, downstream commands may break. The following simple example demonstrates this difference clearly.</p><pre><code class="language-bash">python3 -c 'import sys; print("123"); print("err", file=sys.stderr)' | grep 123
</code></pre><p>In this command, <code>grep</code> receives only stdout, so it correctly processes only <code>123</code>. However, if stderr is merged as follows, the downstream input changes.</p><pre><code class="language-bash">python3 -c 'import sys; print("123"); print("err", file=sys.stderr)' 2&gt;&amp;1 | grep 123
</code></pre><p>Here, <code>grep</code> receives stderr along with stdout as input. In this example, it may not seem like a major issue. However, in tasks where the format of stdout matters—such as JSON output, CSV output, or numerical aggregation—this mixing immediately becomes a source of failure. In other words, stdout is not just “visible output,” but sometimes a <strong>data channel consumed by the next stage</strong>. In such cases, separating stderr is not a matter of preference but a structural requirement.</p><p>Conversely, there are situations where you want to preserve the entire execution log in batch processing. In such cases, storing stdout and stderr together may be more useful for analysis. For example, if you need to reconstruct context around a failure point, a unified stdout/stderr log is more appropriate. Therefore, practical decision-making does not end with a general rule of “merge or not.” You must first determine whether stdout is a message for humans, data for another process, or a simple log. Then decide whether stderr is an independent error channel or should be preserved together with execution context. If you attach <code>2&gt;&amp;1</code> habitually without making this decision, it may be convenient in some cases but will immediately break the structure in others.</p><p>To manage this reliably, you must define the role of each FD before execution. Determine whether stdout is a data channel, an execution log, or a simple user message. Determine whether stderr is an independent failure channel or a supplementary log to be included in the overall execution context. Only after that should you choose redirection syntax. If you reverse this order, the syntax may be correct, but the structure will be unstable. Ultimately, what is required in practice is not the ability to combine commands, but the ability to <strong>design execution state</strong>.</p><p>▶ To understand this concept more deeply:&nbsp;<a href="https://en-signal.ceak.dev/linux-log-redirection-strategy-stdout-stderr/" rel="noreferrer">Linux Log Redirection Strategy — When to Combine and When to Separate stdout and stderr</a></p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-122.png" class="kg-image" alt="" loading="lazy" width="1024" height="559" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-122.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-122.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-122.png 1024w" sizes="(min-width: 720px) 720px"></figure><p>At this point, what remains is to organize practical intuition into a single model. In the next section, a minimal set of questions must be clearly established so that the behavior of any new command can be predicted.</p><h2 id="what-matters-now-is-not-syntax-but-a-predictable-model">What matters now is not syntax, but a predictable model</h2><p>The reason this discussion has taken a long path is simple. What is required to actually handle Linux I/O is not memorization of command fragments, but a model that allows you to visualize the state just before execution. Regardless of the command, the questions to ask are always the same. What does fd 0 point to? What does fd 1 point to? What does fd 2 point to? And in what order were these connections established? If you can answer these four questions, there is no need to memorize every pattern. Even if <code>2&gt;&amp;1</code> looks unfamiliar, knowing that stderr references the current target of stdout allows you to compute the result. Even if pipes span multiple stages, tracing where each process’s stdin and stdout are connected allows you to explain the behavior. Whether redirection points to a file, device, <code>/dev/null</code>, or pipe, the principle does not change. Only the target of the connection changes.</p><p>To verify that this model is actually valid, the best approach is to look at a new command and predict its behavior before running it. For example, consider the following command.</p><pre><code class="language-bash">grep foo input.txt 2&gt; err.log | sort &gt; out.txt
</code></pre><p>If you try to memorize this in functional terms, it may be confusing. But through the model, the interpretation is clear. The stdin of the first process <code>grep</code> comes from the default input or file argument. Its stdout is connected to the write end of a pipe. Its stderr is connected to the file <code>err.log</code>. The stdin of the second process <code>sort</code> is connected to the read end of the pipe. Its stdout is connected to the file <code>out.txt</code>. From this interpretation, you can predict that errors from <code>grep</code> will not be passed to <code>sort</code>, and that <code>sort</code> will receive only the normal stdout of <code>grep</code> as input. This prediction must match the execution result. That is, this model is not just a conceptual explanation—it is a practical computation tool.</p><p>At this level, syntax is no longer something to memorize, but a notation for state changes. <code>&gt;</code> is notation for changing the stdout target. <code>2&gt;</code> is notation for changing the stderr target. <code>2&gt;&amp;1</code> is notation for making stderr reference the current target of stdout. <code>|</code> is notation for connecting the stdout of one process and the stdin of another process to the two ends of a pipe object. At this point, users no longer become confused by results. Instead, they begin by asking, “what FD wiring did the shell construct before executing this command?” The moment this question appears, it means I/O is no longer being read as isolated command syntax, but as system behavior.</p><p>The strength of this model lies in its generality. Even when encountering other topics such as stdout/stderr separation strategies, advanced redirection, job control, or background execution, the same questions can be applied. Ultimately, understanding Linux I/O is not about memorizing <code>&gt;</code> or <code>|</code>. It is about <strong>being able to predict the input/output state constructed just before execution</strong>. And from that point on, syntax is no longer a set of disconnected rules, but a form of expression built on a single consistent state model.</p><p>▶ To understand this concept more deeply:&nbsp;<a href="https://en-signal.ceak.dev/linux-shell-execution-structure-complete-guide/" rel="noreferrer">Complete Guide to Linux Shell Execution — How Programs Are Executed and Controlled</a></p>]]></content:encoded>
                </item>
                <item>
                    <title>The Moment GitHub Turned Open Source into a Platform: Not a Code Repository, but a Social Network for Developers</title>
                    <link>https://en-signal.ceak.dev/00080202-github-open-source-platform-revolution/</link>
                    <guid isPermaLink="true">https://en-signal.ceak.dev/00080202-github-open-source-platform-revolution/</guid>
                    <pubDate>Sat, 11 Apr 2026 15:02:27 +0900</pubDate>
                    <dc:creator><![CDATA[ceak]]></dc:creator>
                    <category><![CDATA[⚙ Essays]]></category><category><![CDATA[📚 Defining Moments in Software History]]></category><category><![CDATA[📚 Tools That Changed Developer Culture]]></category><category><![CDATA[Github]]></category><category><![CDATA[Software History]]></category><category><![CDATA[Open Source]]></category>
                    <description><![CDATA[GitHub was not just a code repository. Through Pull Requests, forks, and social coding, it transformed open source into a platform and redefined the very way developers collaborate. This article traces that turning point as a single, connected narrative.]]></description>
                    <content:encoded><![CDATA[<h2 id="the-world-before-github-%E2%80%94-why-open-source-was-difficult">The World Before GitHub — Why Open Source Was Difficult</h2><p>Before GitHub emerged, the world of open source looked very different from what we are familiar with today. Developers today can explore projects, modify code, and contribute through Pull Requests with just a few clicks in a browser. However, until the early 2000s, participating in open source projects involved a much more complex and closed structure. While the code itself was open, <strong>the process of participating in that code was not truly open</strong>. This contradictory structure became one of the biggest barriers to the growth of open source.</p><p>At the time, developers primarily collaborated through mailing lists. To contribute to a project, one had to modify the code, generate a patch file, and send it via email. Then, a project maintainer would review the patch and manually apply it to the codebase. This process required more than just technical skill. It demanded an understanding of the project’s rules, communication style, and sometimes even its implicit culture. In other words, writing good code was not enough—<strong>one had to adapt to the internal culture of the project to contribute</strong>.</p><p>The problem was that this structure naturally created a high barrier to entry. For new contributors, mailing lists felt unfamiliar and difficult, and patch-based collaboration was far from intuitive. Moreover, because the process of code changes was not structurally visible, it was difficult to understand what others were working on. As a result, many open source projects remained <strong>centered around a small group of core developers</strong>.</p><p>The collaboration environment of that time was also highly fragmented in terms of tools. Code repositories were hosted on CVS or Subversion, issue tracking was handled by separate systems like Bugzilla, and discussions took place on mailing lists. To fully understand a single change, one had to navigate across multiple systems, which disrupted the flow of collaboration. Ultimately, while open source was technically accessible, it functioned more like a <strong>semi-open system that was difficult to participate in</strong>.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-9.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-9.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-9.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-9.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>This structure did contribute to the growth of open source to some extent, but it also clearly exposed its limitations. The fact that code is open and the fact that anyone can easily participate are fundamentally different things. And at this point, a critical question naturally emerges. <strong>If the code is open, why is collaboration still closed?</strong> This question would later become an important foundation for the emergence of Git and GitHub.</p><h2 id="git-existed-but-collaboration-was-still-difficult">Git Existed, But Collaboration Was Still Difficult</h2><p>One of the attempts to solve these problems was the emergence of Git. Git introduced a completely different approach compared to traditional centralized version control systems. Each developer could have a full copy of the repository, and branching and merging became highly flexible. This was a revolutionary concept at the time, especially considering that Git was created to address collaboration issues in Linux kernel development. In this sense, Git can be understood as a tool that provided the <strong>technical foundation for collaboration</strong>.</p><p>The most important characteristic of Git was its distributed nature. Development was no longer dependent on a central server, and all operations could be performed offline. Additionally, branching became lightweight, allowing for parallel work and experimentation. In theory, this created a much more flexible and scalable collaboration model. However, reality turned out to be different. Git was powerful, but <strong>that power translated into complexity for users</strong>.</p><p>Git was a command-line-based tool, and in its early days, it was not easy to use. Concepts like commit, rebase, and merge were not intuitive, and resolving conflicts required a deep understanding. More importantly, Git itself lacked a proper interface for collaboration. It was purely a version control tool and did not provide visual representations of changes or integrated communication features. In other words, <strong>Git made collaboration possible, but it did not make collaboration easy</strong>.</p><p>As a result, even with Git, many projects continued to rely on mailing lists and patch-based workflows. While Git repositories existed, code reviews and communication were still handled through traditional methods. This highlighted a clear limitation—there were aspects of collaboration that Git alone could not solve. The technical foundation was in place, but <strong>the way people collaborated had not yet changed</strong>.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-10.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-10.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-10.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-10.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>At this point, a critical transition was needed. What was required was not just a better version control system, but a new approach that could redefine collaboration itself. Git had laid the groundwork, but it needed a new layer on top—a layer that could transform how people actually worked together. And that role would be fulfilled by GitHub.</p><h2 id="the-emergence-of-github-%E2%80%94-it-was-not-just-a-hosting-service">The Emergence of GitHub — It Was Not Just a Hosting Service</h2><p>When GitHub appeared in 2008, it initially looked like a simple Git repository hosting service. However, it carried a much deeper transformation. GitHub was a service that placed <strong>a collaborative interface and user experience on top of Git</strong>. It was an attempt to hide the complexity of Git and make collaboration intuitive.</p><p>The core of GitHub was not the code itself, but the <strong>interactions surrounding the code</strong>. It allowed developers to see who made changes, in what context those changes were made, and what discussions took place around them—all in a single space. Previously, code, issues, and discussions were separated across different systems. GitHub unified them. This was not just an improvement in convenience—it was a <strong>redefinition of the collaboration structure itself</strong>.</p><p>GitHub also made Git significantly more accessible through its web-based interface. Developers no longer needed to master complex commands to review changes, compare code, or participate in discussions. This lowered the barrier to entry for Git and transformed it into <strong>a collaboration tool accessible to anyone</strong>, rather than something reserved for experienced developers.</p><p>What fundamentally distinguished GitHub from previous services was that it turned collaboration from a “feature” into an “experience.” It was no longer just about uploading and downloading code, but about creating an environment where people could communicate and collaborate around that code. This structure would later expand into concepts such as Pull Requests, Forks, and Social Coding, ultimately reshaping the entire open source ecosystem.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-11.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-11.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-11.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-11.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>The emergence of GitHub was not merely the arrival of a new service. It marked the beginning of a fundamental transformation in how open source collaboration worked. And from this point on, that transformation would evolve beyond simply sharing code into building an entirely new <strong>collaborative culture centered around code</strong>.</p><h2 id="pull-request-%E2%80%94-the-feature-that-standardized-code-review">Pull Request — The Feature That Standardized Code Review</h2><p>One of the most decisive changes brought by GitHub was the establishment of the concept known as the <strong>Pull Request</strong>. Until then, code changes were typically made by creating patch files and sending them, or by a small number of developers with direct repository access applying changes themselves. In such a structure, it was difficult to conduct code reviews in a systematic way, and discussions around changes were often fragmented and scattered. However, the Pull Request provided a structure that grouped code changes into a clear unit and naturally layered discussion and review on top of it. This change was not simply the addition of a feature, but rather <strong>a fundamental redefinition of the unit of code collaboration</strong>.</p><p>The core of the Pull Request was not just about “showing” changes, but also about “enabling discussion.” It allowed developers to visually compare what code had been added or removed, and to leave comments directly on specific lines. This transformed code review from an abstract concept into <strong>a concrete interactive process</strong>. Previously, reviews were conducted through emails or documents, but now it became possible to have conversations centered directly around the code itself. This structure naturally improved the quality of reviews while also increasing the speed of collaboration.</p><p>More importantly, Pull Requests began to expose collaboration as a visible “process.” Every detail—how a feature was added, what discussions took place during its development—was preserved as part of the project history. This went beyond simple change tracking and became a valuable resource for understanding the context of a project. Ultimately, the Pull Request transformed code review from a simple verification step into <strong>a structure where collaboration and learning happen simultaneously</strong>.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-12.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-12.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-12.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-12.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>As this structure spread, more and more projects began to adopt Pull Requests as the standard collaboration method. Writing code was no longer the only important skill; <strong>how that code was shared and reviewed</strong> became equally critical. The Pull Request provided the most intuitive answer to that question and soon became deeply embedded not only in open source but also in internal development processes within companies.</p><h2 id="the-fork-button-%E2%80%94-the-moment-that-lowered-the-barrier-to-entry">The Fork Button — The Moment That Lowered the Barrier to Entry</h2><p>If Pull Requests changed the way collaboration worked, the <strong>Fork button changed the very act of entering collaboration itself</strong>. Even before GitHub, it was technically possible to copy and modify code, but the process was not clearly structured, and there was always a psychological barrier involved. In particular, the fear of directly impacting the original project often discouraged developers from contributing. The Fork feature addressed this problem in a simple yet powerful way. With a single click, an entire project could be copied into a developer’s own repository, and all subsequent work could be carried out independently.</p><p>This structure gave developers a crucial kind of freedom. They could experiment with new ideas without affecting the original project and try different approaches without the fear of failure. Such an environment naturally encouraged greater participation. In the past, contributing required adapting to a project’s rules and culture, but now it became possible to <strong>experiment independently first and share later</strong>. This marked a fundamental shift in the paradigm of open source participation.</p><p>Another important implication of Fork was that it enabled projects to diverge and evolve in multiple directions. A single project could branch into entirely different paths, sometimes even becoming completely new projects. This was not just code duplication; it functioned as <strong>a mechanism for ecosystem expansion</strong>. Cases such as MariaDB branching from MySQL or LibreOffice splitting from OpenOffice demonstrate how this structure could lead to entirely new ecosystems.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-13.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-13.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-13.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-13.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>Ultimately, the Fork button went beyond being a technical feature and acted as a mechanism that provided psychological safety to developers. An environment where anyone could start easily and failure carried no consequences led to an explosive increase in participation. This transformation allowed open source to move beyond a niche domain and grow into <strong>a large-scale, participation-driven ecosystem</strong>.</p><h2 id="social-coding-%E2%80%94-the-moment-development-became-a-public-network">Social Coding — The Moment Development Became a Public Network</h2><p>Another fundamental transformation brought by GitHub was turning development into <strong>a social activity</strong>. Previously, development was primarily carried out individually or within small teams, and much of the process remained invisible to the outside world. While open source projects were technically public, the internal activities were not shared in real time, nor were they interconnected in a networked form. GitHub completely overturned this structure. Code creation and collaboration processes became fully visible, and developers could observe each other’s activities in real time.</p><p>On GitHub, interactions extended far beyond simply uploading code. Developers could “Star” projects they found interesting, “Follow” other developers, and track ongoing activities through feeds. These features transformed development into a form of content, and developers could showcase their skills through that content. This shift redefined development from a closed activity into <strong>an open and interconnected networked practice</strong>.</p><p>This transformation also had a significant impact on the identity of developers. A GitHub profile became more than just an account; it became a record of a developer’s work and a portfolio. Contributions to projects, code written, and problems solved were all publicly visible, and these elements began to serve as key indicators of a developer’s capability. As companies increasingly started evaluating developers based on their GitHub activity, GitHub effectively became <strong>a platform that validates a developer’s career</strong>.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-14.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-14.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-14.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-14.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>In the end, Social Coding was not just a collection of features but a shift in how development itself was perceived. Code was no longer an isolated output of an individual but became <strong>part of a shared and evolving network</strong>. This transformation laid the foundation for GitHub to grow beyond a simple tool into the central platform of the modern development ecosystem.</p><h2 id="why-github-became-a-platform">Why GitHub Became a Platform</h2><p>As Pull Requests, Forks, and Social Coding converged into a single flow, GitHub began to move beyond the role of a simple code repository. It initially started as a hosting service that made Git easier to use, but as more projects and developers gathered, GitHub naturally evolved into the <strong>central hub of the development ecosystem</strong>. This transformation was not something that was deliberately designed from the beginning, but rather the result of features and users continuously reinforcing each other. Where code existed, people gathered, and where people gathered, even more code accumulated.</p><p>This structure closely resembles the growth pattern of a typical platform. GitHub was no longer just a place to store code, but a space where <strong>interactions centered around code continuously occurred</strong>. Developers uploaded projects, others forked them, and collaboration happened through Pull Requests. In this process, not only code but also knowledge and experience were shared together. Ultimately, GitHub evolved from being merely a “code repository” into a <strong>network platform mediated by code</strong>.</p><p>Another reason GitHub became a platform lies in its <strong>scalability</strong>. On GitHub, not only simple libraries but also frameworks, operating systems, and even infrastructure-level projects could be managed. Projects like React, Kubernetes, and TensorFlow grew rapidly on GitHub, creating a structure where developers from around the world could participate simultaneously. This made it possible for technology to evolve not within a single company or organization, but within a <strong>global collaborative network</strong>.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-15.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-15.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-15.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-15.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>Ultimately, GitHub became not just a service but a <strong>platform layer connecting developers and projects</strong>. As countless technologies were created and spread on top of this platform, GitHub established itself as a core infrastructure of the modern software ecosystem.</p><h2 id="why-developers%E2%80%99-portfolios-moved-to-github">Why Developers’ Portfolios Moved to GitHub</h2><p>The expansion of GitHub did not only affect how projects were collaboratively developed. It also brought a direct transformation to individual developers’ careers and identities. In the past, resumes and interviews were the primary means of evaluating a developer’s skills. However, with the emergence of GitHub, developers were able to publicly showcase their code and demonstrate how it was actually being used. This introduced a new way to present a developer’s capabilities in a more objective and tangible manner.</p><p>A GitHub profile became more than just an account; it turned into a space where a developer’s activity history accumulated over time. Which projects they contributed to, how consistently they were active, and what kinds of problems they solved were all openly visible. Through this process, developers were no longer evaluated simply as people who “could do something,” but rather as individuals who had <strong>actually built and contributed to real projects</strong>. This fundamentally changed how developers were assessed.</p><p>At the same time, GitHub strengthened connections between developers. It became possible to learn from others’ code or build relationships through direct contributions. This structure transformed developer growth from being dependent on individual experience into a process that expanded through a <strong>network of collaboration and interaction</strong>. GitHub thus evolved beyond a collaboration tool into a platform that actively shaped developers’ careers.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-16.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-16.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-16.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-16.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>This transformation also influenced how companies approached hiring. Many organizations began to evaluate developers based on their GitHub activity, and open source contributions became an important part of a developer’s credentials. Ultimately, GitHub reshaped both career development and evaluation systems, becoming a <strong>platform that expanded the very meaning of being a developer</strong>.</p><h2 id="after-github-%E2%80%94-how-open-source-changed">After GitHub — How Open Source Changed</h2><p>The emergence of GitHub accelerated changes across the entire open source ecosystem. Previously, open source projects operated within limited participation structures, but after GitHub, an environment was created where anyone could easily contribute. This shift was not just about increasing the number of participants, but about <strong>fundamentally changing how projects grew and evolved</strong>. With more people contributing more quickly, the pace of technological advancement also increased noticeably.</p><p>Open source projects also became less dependent on specific organizations or communities. On GitHub, projects could operate as independent entities, enabling simultaneous participation from developers around the world. This marked a shift where technology was no longer an internal asset of a particular company, but instead evolved into a <strong>shared public asset developed through global collaboration</strong>. GitHub was the key infrastructure that made this transformation possible.</p><p>Alongside this, the role of open source itself expanded. It was no longer just a source of free code, but a space where new technologies could be experimented with and validated. Both startups and large corporations began to release technologies on GitHub and develop them in collaboration with the community. This indicated a broader shift in which the center of technological development moved from inside organizations to the outside.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-17.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-17.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-17.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-17.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>Ultimately, open source after GitHub became not just a development approach, but a culture and an industry structure in itself. And this transformation leads to the next stage. Developers are no longer individuals writing code in isolation, but <strong>members of a network who collaborate and grow on top of a platform</strong>. This flow becomes even clearer in the story that follows.</p><h2 id="conclusion-%E2%80%94-github-was-not-a-tool-but-an-event-that-changed-development-culture">Conclusion — GitHub Was Not a Tool, but an Event That Changed Development Culture</h2><p>If you follow the flow up to this point, a clear pattern begins to emerge. GitHub did not create a new programming language, nor did it invent an entirely new technology that had never existed before. Git, as a powerful version control system, was already there, and the concept of open source had been around for quite some time. And yet, GitHub created a level of change significant enough to be called an “event.” The reason is simple: GitHub did not change the technology itself, but rather <strong>reconstructed the way people collaborate on top of that technology</strong>.</p><p>Even before GitHub, code was being shared, but that sharing was limited and incomplete. Participation was difficult, collaboration was fragmented, and records were scattered across different systems. GitHub brought all of these processes together into a single continuous flow. Writing code, making changes, reviewing, discussing, and even deploying began to happen seamlessly on one platform. This shift went beyond mere convenience; it fundamentally altered <strong>the structure of development as an activity itself</strong>. Development was no longer an isolated task performed by individuals, but was redefined as a collaborative process taking place on a platform.</p><p>GitHub also transformed development into something that is <strong>visible and observable</strong>. Code, commits, reviews, and discussions all became publicly accessible, meaning developers were no longer working in isolation behind the scenes. GitHub profiles and activity histories became part of a developer’s identity, directly influencing their career trajectory. In this way, GitHub evolved beyond a simple collaboration tool and began functioning as a platform that <strong>reshaped the social structure of what it means to be a developer</strong>. The change was centered not on technology, but on people, which is why its impact spread so deeply and widely.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-18.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-18.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-18.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-18.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>Ultimately, the most important change GitHub brought was the creation of an environment where <strong>anyone could participate in development</strong>. Through Fork and Pull Request, the barriers to contribution were dramatically lowered, and through Social Coding, developers became interconnected. This structure transformed open source from a simple method of sharing code into <strong>a global collaborative ecosystem</strong>. That ecosystem continues to expand, with new technologies and projects constantly emerging from within it.</p><p>Development is no longer an activity confined within the boundaries of a specific organization or company. On platforms like GitHub, developers from around the world collaborate simultaneously to build technology together. This transformation was not merely an evolution of tools, but rather <strong>a fundamental shift toward network-centered development itself</strong>. And this shift does not end here. In the next chapter, another significant change begins to unfold. As knowledge, rather than code itself, becomes the central focus, development culture once again starts to be redefined.</p>]]></content:encoded>
                </item>
                <item>
                    <title>Readium Devlog #2 — Why Domain Models Inevitably Collapse</title>
                    <link>https://en-signal.ceak.dev/readium-devlog-2-domain-model-collapse/</link>
                    <guid isPermaLink="true">https://en-signal.ceak.dev/readium-devlog-2-domain-model-collapse/</guid>
                    <pubDate>Fri, 10 Apr 2026 17:52:01 +0900</pubDate>
                    <dc:creator><![CDATA[ceak]]></dc:creator>
                    <category><![CDATA[📱 Apps]]></category><category><![CDATA[📚 Readium Development Log — Why Design Keeps Failing]]></category><category><![CDATA[Domain Modeling]]></category><category><![CDATA[Software Architecture]]></category><category><![CDATA[Event Driven Design]]></category>
                    <description><![CDATA[Domain models don’t fail by accident—they fail by design. This post traces how a seemingly correct state-based model collapses, and why separating state, time, and events becomes inevitable.]]></description>
                    <content:encoded><![CDATA[<h2 id="the-sentence-%E2%80%9Creading-is-a-flow%E2%80%9D-was-a-result">The sentence “Reading is a flow” was a result</h2><p>The sentence “Reading is a flow” now looks like a fairly solid claim. It almost feels as if the design started with that concept at its core from the very beginning. But the actual process was nothing like that. This sentence was not a starting point, but rather something close to a conclusion that remained after several rounds of design failure. At first, there was only an intention to resolve discomfort, and even that discomfort itself was not clearly defined. So the initial design was created quickly, relying on intuition and experience. And that intuition did not hold up for very long.</p><p>At the beginning, the goal was simply to solve the problem that “records do not remain.” However, while building a structure to solve that problem, it became clear that I could not even clearly explain what I actually wanted to see. I knew that records were necessary, but I had not defined what form those records should take. Naturally, this led me to follow structures similar to those used by existing services. Storing state, updating progress, and marking completion—this approach did not seem wrong, and it was not difficult to implement. The problem was that this structure did not align with the actual user experience, and that mismatch only became apparent later.</p><p>In the end, the concept of “flow” was not something that existed from the beginning, but rather something that emerged when the existing structure repeatedly failed to answer questions. Questions began to accumulate—why was there no reading on certain days, why were some books abandoned midway, why was the reading pattern inconsistent. As these questions piled up, the existing model gradually lost its explanatory power. Only then did the realization emerge that this had to be viewed not as state, but as a sequence of time and change. This sentence may look like a philosophy, but in reality, it was the minimum explanation left when the design could no longer hold. Because the starting point was incomplete, every design that followed had already begun on a flawed foundation.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-91.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-91.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-91.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-91.png 1536w" sizes="(min-width: 720px) 720px"></figure><h2 id="why-the-initial-design-looked-so-natural">Why the initial design looked so natural</h2><p>The initial domain design was surprisingly simple. There was an entity called Book, which contained state and progress, and reading behavior was managed through sessions. This structure was intuitive, easy to explain, and not difficult to implement. In fact, many services use similar models, so it did not feel unfamiliar at all. It was almost more unnatural to question this structure. When the initial design was drawn, this simplicity was even considered a strength. It was believed to be a clean structure that removed unnecessary complexity.</p><p>The problem was that this simplicity was not the result of accurately reflecting the domain. It looked simple because the concepts were not clearly separated. State and record, current value and past changes, time and result—all of these were mixed within a single model, yet the differences were not recognized, and it was simply assumed that they could be expressed with a single field. For example, progress appears to be an attribute of a book, but in reality, it is a value that continuously changes over time. The moment it is stored as a fixed number, critical information is already lost. This loss is not immediately visible, but it becomes a problem as soon as functionality expands even slightly.</p><p>Another reason is that this structure aligns very well with CRUD-centric applications. Storing state, updating values, and retrieving them is extremely familiar to developers. As a result, the domain is naturally interpreted within that framework. However, the act of reading cannot easily be reduced to simple state changes. Reading is not a value at a specific point in time, but rather an accumulation of time and a sequence of changes. Nevertheless, the initial design compressed this complex concept into a simple state model. As a result, the structure appeared clean, but its explanatory power was insufficient.</p><p>In the end, this initial design was a case of “incorrect simplification.” It was not that a complex domain was made simple, but rather that essential dimensions were removed, making it only appear simple. That is why it seemed fine at first, but cracks began to appear as soon as real data was handled. At this point, the structure had not completely collapsed yet, but there were already enough signals indicating that its foundation was unstable. And that first signal was progress.</p><h2 id="the-first-crack-%E2%80%94-progress-is-not-a-state">The first crack — progress is not a state</h2><p>When designing progressPct for the first time, there was almost no hesitation. It seemed entirely natural that if someone is reading a book, there should be a value indicating how far they have progressed. So this value was naturally included in the Book entity. It was considered a field representing “the current state of this book,” and nothing beyond that was deeply considered. However, as actual usage scenarios were followed step by step, it quickly became clear how fragile this assumption was.</p><p>A user reads up to 20% in a day and ends the session. The next day, they read up to 35% and end another session. Up to this point, there is no issue. But imagine that a few days later, the user mistakenly inputs 30%. The progress value stored in Book is simply overwritten to 30%. The fact that the user had previously reached 35% disappears. The information that on certain days almost no reading occurred also disappears. In the end, only a single number remains. This number appears to represent the current state, but in reality, it is the result of erasing the past. What I stored was not a “current value,” but rather the removal of “traces of change.”</p><p>At this point, an important realization emerges. Progress is not a simple state value, but a result at a specific point in time. In other words, progress does not exist independently, but is a value dependent on the time unit called a session. Only when progress is recorded at the moment a session ends does it gain meaning. Only then can the flow of change be traced. Otherwise, all changes are compressed into a single final value. This is not just data loss, but a loss of the essence of the domain itself.</p><p>This single field began to shake the entire design. The moment progress is placed inside Book, Book implicitly becomes responsible not only for the current state but also for past history. However, that responsibility is not properly fulfilled. As a result, the role the model is supposed to handle and the actual implementation begin to diverge. This divergence is not immediately visible, but it grows as features accumulate. And at this point, one thing becomes clear. The current structure is not simply in need of improvement—it is in a state that requires fundamental reconsideration.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-92.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-92.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-92.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-92.png 1536w" sizes="(min-width: 720px) 720px"></figure><h2 id="the-fundamental-limitation-of-the-state-model-%E2%80%94-%E2%80%9Conly-the-present-exists%E2%80%9D">The Fundamental Limitation of the State Model — “Only the Present Exists”</h2><p>The problem with progress was not a simple implementation mistake, but a signal that revealed the limitation of the model itself. At first, it seemed like a matter of placing a field incorrectly, but with even a slightly deeper look, it became clear that the root of the issue lay somewhere entirely different. I had been taking the concept of state for granted, assuming that it could sufficiently represent the domain. However, state is always a compressed result at a specific point in time. It does not contain the process of change. State is useful for representing outcomes, but structurally incapable of capturing the process itself.</p><p>When design proceeds without understanding this limitation, a single model inevitably tries to explain multiple points in time simultaneously. The status and progress stored in Book represent the current state, but at the same time, they implicitly take on the responsibility of representing past flow as well. However, the state model cannot fulfill that responsibility. Past information disappears through overwriting, and the context of change is completely lost. In this structure, it becomes impossible to answer the question, “why did this state occur?” State can only speak about “now,” and everything before that is erased.</p><p>This limitation is not unique to a reading app. State-based models are commonly used in most systems, but in domains where the flow of time is essential, they inevitably create the same problem. State compresses data, and in that compression process, meaning is lost. That loss accumulates gradually, and at some point, it begins to shake the entire structure. I did not understand this theoretically at first; I experienced it through the gradual loss of explanatory power in the design. In the end, the state model was not wrong—it was <strong>being applied to a domain where it could not be used</strong>.</p><p>At this point, a critical shift occurs. The question is no longer “how can we manage state better,” but rather “is this problem even appropriate to express as state?” This question completely changes the direction of the design. From that moment on, the existing model no longer appears as an extensible structure, but as something that must inevitably collapse. The crack that began with progress ultimately led to questioning the entire state model.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-93.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-93.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-93.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-93.png 1536w" sizes="(min-width: 720px) 720px"></figure><h2 id="the-second-crack-%E2%80%94-collision-with-the-event-model">The Second Crack — Collision with the Event Model</h2><p>As I began to recognize the limitations of the state model, I naturally started exploring other directions. One of them was the timeline feature. I wanted to show when a user started reading, when they stopped, and at what point they completed the book, all in chronological order. This feature may appear to be just a UI element, but in reality, it requires a completely different model. Instead of state, it requires recording events. So I began defining events such as SESSION_STARTED, SESSION_ENDED, and RECORD_COMPLETED, and built the timeline based on them.</p><p>At first, this approach seemed like it could complement the existing model. I thought that keeping the state as it was while additionally recording events would provide richer information. However, as implementation progressed, it became clear that these two models were in conflict. A representative example was the relationship between session termination and completion events. In most cases, the moment a user finishes a book coincides with the moment a session ends. Should these two events be recorded simultaneously? Or should they be merged into one? This question was not a simple implementation issue, but a signal that the boundary between the models was unclear.</p><p>This problem did not stop at an increase in the number of events. Situations emerged where the outcome differed depending on the order of events, and cases appeared where processing the same action twice produced different results. In other words, the system became non-idempotent. This was a dangerous state that could lead to data inconsistency. The state model and the event model interpreted the same action based on different criteria, causing a single action to carry two meanings. This conflict generated more and more edge cases, and as conditions to handle those cases increased, the design became progressively more complex.</p><p>At this point, I could no longer think in terms of “how to combine these two well.” The problem was not integration, but that <strong>two fundamentally different concepts were being placed in the same layer</strong>. State represents results, while events record processes. They serve different roles and cannot replace each other. Yet they were being handled together within a single model, which inevitably led to conflict. This crack was structural, and not something that could be resolved with simple refactoring.</p><h2 id="the-moment-of-collapse-%E2%80%94-the-model-can-no-longer-handle-questions">The Moment of Collapse — The Model Can No Longer Handle Questions</h2><p>The moment you realize a design is truly wrong is not when the code fails to run, but when it cannot answer questions. I began asking more and more questions. “Why did this user stop reading at this point?”, “What pattern did the session have right before completion?”, “How should periods of inactivity be represented?” These were not feature requirements, but attempts to understand the domain. However, the existing model could not properly answer these questions.</p><p>Consider again the case where session termination and completion occur at the same time. These two events happen at the same moment, but they carry different meanings. Session termination represents the end of time, while completion represents a change in state. Yet in the existing model, it was impossible to clearly separate them. If merged into a single event, information is lost. If separated into two, duplication and ordering issues arise. No matter which choice is made, consistency becomes difficult to maintain. This was not a matter of design preference, but a sign that the model lacked the structural capacity to handle the question.</p><p>As these situations repeated, a clear pattern emerged. Every time a new question appeared, maintaining the existing structure required adding exceptions. And those exceptions created even more exceptions. Eventually, the system became increasingly complex, but its explanatory power declined. The data existed, but what that data meant became increasingly unclear. At this stage, adding features was no longer progress, but an expansion of the problem itself. The design was no longer a foundation for growth, but a structure that only increased maintenance cost.</p><p>At this point, I had to make a decision. Should I continue patching the existing structure, or redefine it fundamentally? The answer was not difficult. The model had already failed to handle the questions. This was not an issue of improvement, but of collapse. In the end, I abandoned the existing structure and chose to rethink everything from the beginning. This decision was not a simple refactoring, but a turning point that changed the very perspective from which I viewed the domain.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-94.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-94.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-94.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-94.png 1536w" sizes="(min-width: 720px) 720px"></figure><h2 id="the-criterion-for-redesign-%E2%80%94-%E2%80%9Cseparation-of-responsibilities%E2%80%9D">The Criterion for Redesign — “Separation of Responsibilities”</h2><p>The moment you accept that the existing model can no longer handle the questions being asked, the next step follows naturally. The problem is no longer what to change, but <strong>what criteria should guide the redesign</strong>. Up to this point, the issue was not a mistake in a specific field or an error in defining events. Different concepts had been mixed together within a single model, and as a result, each element failed to fulfill its own role clearly. Therefore, the starting point of the redesign had to be not functionality, but <strong>the separation of concepts</strong>.</p><p>The principle here is simple. A single model should answer only a single type of question. This sounds obvious, but in actual design, it frequently breaks down. The Book model was supposed to represent state, yet it implicitly carried the responsibility of tracking the history of progress. The Session model was supposed to represent time, yet it also tried to absorb state changes. Once responsibilities begin to overlap like this, it becomes impossible to clearly explain which model is responsible for what. And that point is precisely where structural collapse begins.</p><p>So instead of dividing features, I began dividing questions. “What is the current state?”, “When did a particular action occur?”, “What changes happened over time?” These three questions do not overlap. Each requires a different form of data. State is expressed as a single value, time must be expressed as a duration, and events must be recorded with order. The moment this distinction is acknowledged, maintaining the original model starts to look inefficient rather than simple.</p><p>This criterion does not make the design simpler; it makes it stricter. Each model must be constrained so that it never crosses into responsibilities it does not own. However, this constraint actually stabilizes the structure. Since no single model is burdened with multiple roles, adding new functionality no longer requires destabilizing the existing structure. Ultimately, the essence of the redesign was not about creating a new structure, but about <strong>clearly defining the boundaries of responsibility for each concept</strong>.</p><h2 id="record-session-event-%E2%80%94-not-structure-but-perspective">Record / Session / Event — Not Structure, but Perspective</h2><p>When the domain was restructured based on this criterion, three axes naturally emerged. Record represents state, Session represents time, and Event records occurrences. These may appear to be new models, but in reality, they are the result of separating concepts that already existed. Previously, all of these were compressed into a single structure called Book, and that compression was the source of the problem. Now, the process was about decompressing that structure and allowing each concept to exist independently.</p><p>ReadingRecord is the simplest. This model expresses only the current state. Whether the book is being read, completed, or abandoned. It contains no time information and no history. Instead, it focuses solely on a clear definition of state. ReadingSession serves a completely different role. It records when something started, when it ended, and what changes occurred during that session. It is the combination of time and measurement. Finally, TimelineEvent records facts. It logs what happened in sequence, without interpreting their meaning.</p><p>What matters here is that these three models do not replace each other. Record explains the state but does not explain how that state came to be. Session records the process but does not determine the current state. Event lists occurrences but does not compute outcomes. Each model is strictly limited to its role and does not cross its boundaries. This structure may initially appear more complex, but in reality, it is much simpler, because each model has a clearly defined purpose.</p><p>This redefinition is not merely a structural change; it is a shift in perspective. The focus is no longer on “where should this data be stored,” but on “what kind of information is this data.” As that definition becomes clearer, conflicts between models naturally disappear. At this point, the design begins to transform from an unstable structure into a scalable foundation.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-95.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-95.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-95.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-95.png 1536w" sizes="(min-width: 720px) 720px"></figure><h2 id="removing-progress-%E2%80%94-the-moment-the-domain-becomes-lighter">Removing Progress — The Moment the Domain Becomes Lighter</h2><p>The most symbolic change in this redesign process was the moment progress was removed from the Book model. At first, this decision felt counterintuitive. Progress is one of the most intuitive pieces of information for users. Removing it from Book seemed like giving up the simplest way to represent the current state. But in reality, it was the opposite. This removal was not about reducing functionality; it was about <strong>clarifying responsibility</strong>.</p><p>Once progress is moved into Session, its meaning fundamentally changes. It is no longer a value that represents “how far the user has read so far,” but a value that records “how far the user had read at the end of this session.” This shift is not just about moving a field; it is about redefining the nature of the data itself. Progress is no longer a single state value but becomes a series of points distributed across time. And only by connecting these points does the actual reading flow emerge.</p><p>As a result, the Book model becomes significantly lighter. It no longer manages values that change over time and instead maintains only the current state. This simplification stabilizes the design. Previously, every update to progress required consideration of both state and history. Now, that burden is gone. Session takes full responsibility for change, and Record reflects only the result. With this separation, the relationships between data elements become much clearer.</p><p>This experience reveals an important truth. Design is determined not by what you add, but by what you remove. Initially, the attempt was to expand the model to hold more information. In reality, what stabilized the structure was the process of removing unnecessary responsibility. The moment progress was removed, the domain became simpler, yet capable of explaining more. This paradox demonstrates how much unnecessary complexity is introduced by flawed design. And at this point, the new structure begins to establish itself not as an experiment, but as a clear direction forward.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-96.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-96.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-96.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-96.png 1536w" sizes="(min-width: 720px) 720px"></figure><h2 id="design-is-defined-by-sentences-not-code">Design is Defined by Sentences, Not Code</h2><p>By the time the models were separated and the structure was reorganized, the process seemed relatively smooth. As the three axes—Record, Session, and Event—became clearly defined, collisions between fundamentally different concepts were significantly reduced. Yet, despite this structural clarity, there was still a lingering sense that the design was not fully stable. The code itself was becoming cleaner, but decisions about how the system should behave in specific situations remained ambiguous. This ambiguity consistently surfaced during testing, revealing itself in edge cases that had not been anticipated.</p><p>The root of this problem was not in the code, but in the absence of clear definitions. While the structure had been divided, the rules governing how that structure should operate had not been explicitly articulated. For example, when asked “how many sessions can exist simultaneously for a single book,” the code implicitly enforced a limit of one, yet there was no clear explanation for why this constraint existed. Similarly, the question “can the progress value be null” could be technically implemented, but its meaning within the domain remained unclear. In such a state, the more code that was added, the more uncertainty accumulated alongside it.</p><p>This led to a shift in approach: design began to be defined through sentences rather than code. Each model’s constraints were described explicitly, and the behavior of the system under various conditions was written out in detail. This process revealed far more than expected. Decisions such as whether null values should be allowed, how repeated events should be handled, or whether data should be deleted or preserved under certain conditions were no longer seen as implementation details, but as domain definitions. Any rule that could not be clearly expressed in language could not be consistently enforced in code.</p><p>Once this change was made, the design became significantly more robust. This was not because the code had become more complex, but rather because ambiguity had been removed. With every policy clearly defined, new features could be added without reinterpreting the existing structure. Ultimately, the stability of the design was not derived from the quality of the code, but from the <strong>clarity of its definitions</strong>. Design must exist before implementation, and that existence must first take form in language, not in code.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-97.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-97.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-97.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-97.png 1536w" sizes="(min-width: 720px) 720px"></figure><h2 id="conclusion-%E2%80%94-design-is-not-what-is-%E2%80%9Ccorrect%E2%80%9D-but-what-remains">Conclusion — Design is Not What is “Correct,” but What Remains</h2><p>At this point, it becomes inevitable to reflect on the original design. At first, it seemed logically sound. A model where Book contained state and progress, connected to sessions, was intuitive and straightforward to implement. There was nothing obviously wrong with it. However, as time passed, as more questions accumulated, and as more edge cases had to be handled, that design could no longer sustain itself. It was not that the design had been correct—it was that it had <strong>failed to endure</strong>.</p><p>What mattered in this process was not identifying what was “right,” but determining what could survive. Removing progress, separating models, and explicitly defining policies were all acts of eliminating prior assumptions. Through this process, only what could withstand repeated questioning remained. Design is not something that is built by addition, but something that is refined through removal. And that refinement is never a one-time event—it is an ongoing process.</p><p>This is not a story about completing a domain design. Rather, it is a record of how easily design can be wrong, and how that wrongness reveals itself over time. Most initial assumptions were flawed, and those flaws did not appear through implementation, but through the inability to answer questions. This pattern will continue. Future designs will also begin with assumptions, and those assumptions will inevitably be challenged and broken.</p><p>In the next article, the focus shifts to the problems that still remained even after this restructuring, particularly the deeper limitations within the session model. Dividing the structure did not solve everything—in fact, it introduced new layers of questions. This series continues by following how those questions once again forced the design to evolve.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-98.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-98.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-98.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-98.png 1536w" sizes="(min-width: 720px) 720px"></figure>]]></content:encoded>
                </item>
                <item>
                    <title>Why Git Emerged: The Version Control Revolution Created by Linux Developers</title>
                    <link>https://en-signal.ceak.dev/00080201-why-git-was-created-linux-version-control-revolution/</link>
                    <guid isPermaLink="true">https://en-signal.ceak.dev/00080201-why-git-was-created-linux-version-control-revolution/</guid>
                    <pubDate>Fri, 10 Apr 2026 17:09:22 +0900</pubDate>
                    <dc:creator><![CDATA[ceak]]></dc:creator>
                    <category><![CDATA[⚙ Essays]]></category><category><![CDATA[📚 Defining Moments in Software History]]></category><category><![CDATA[📚 Tools That Changed Developer Culture]]></category><category><![CDATA[Git]]></category><category><![CDATA[Version Control]]></category><category><![CDATA[Software History]]></category>
                    <description><![CDATA[Git wasn’t created to change the world. Born from a collaboration crisis in Linux kernel development, it introduced distributed version control and transformed how developers work. This article explores its origins and impact on developer culture.]]></description>
                    <content:encoded><![CDATA[<h2 id="how-development-was-done-before-git">How Development Was Done Before Git</h2><p>Version control in software development was not always the obvious, fundamental concept it is today. In the present, we are accustomed to an environment where every change to code is automatically recorded, where we can revert to any previous state at any time, and where multiple people can work simultaneously while conflicts are systematically managed. However, such an environment only became established relatively recently. Before Git emerged, the development environment was far more primitive, and many aspects relied heavily on the developer’s attention and memory.</p><p>Early developers managed versions by appending suffixes such as <code>_final</code>, <code>_final2</code>, or <code>_real_final</code> to file names. This was not merely a joke but a widely used practice in reality. Since there was no structured way to manage the history of code changes, developers had to manually copy files and rename them to distinguish versions. While this method could somewhat work for individual tasks, it quickly revealed its limitations in collaborative environments. It was nearly impossible to track who modified which part of the code and for what purpose those changes were made.</p><p>To address these issues, early version control systems such as CVS (Concurrent Versions System) and Subversion (SVN) emerged. These systems adopted a centralized structure in which a code repository resided on a central server, and developers interacted with it to update their code. This approach made a basic level of collaboration possible and allowed for a certain degree of tracking code changes. However, these systems were fundamentally centralized, which introduced several constraints. Without network connectivity, work became difficult; branching and merging were complex and costly operations.</p><p>Most importantly, version control systems of that era did not transform the way developers thought about development; they merely added minimal tooling on top of existing workflows. Developers still remained “people who modify files and upload them,” and collaboration continued to be a process filled with conflicts and confusion. As project scale increased, this structure generated even more problems. These limitations became especially pronounced in large-scale open source projects.</p><p>At this point, the key issue was not simply the lack of tools, but that <strong>the development paradigm itself had not evolved</strong>. Collaboration remained difficult, code changes were still risky, and the systems in place could not keep up with the rapid growth of project scale. Eventually, these problems accumulated to a level that could no longer be ignored.</p><p>The first place where this limitation became critical was not in small personal projects, but in massive projects involving developers from around the world. The most representative example of this was the development of the Linux kernel.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image.png 1536w" sizes="(min-width: 720px) 720px"></figure><h2 id="the-problems-faced-by-linux-kernel-development">The Problems Faced by Linux Kernel Development</h2><p>The Linux kernel was not just a software project. It was, in essence, an ecosystem with thousands of developers participating simultaneously. Each developer wrote code in different environments, produced changes at their own pace, and those results ultimately had to be integrated into a single kernel. In such a structure, the most critical problem was not simply writing code, but <strong>how to integrate and manage those changes</strong>.</p><p>In its early days, Linux development was centered around mailing lists. Developers shared patches they created via email, and other developers reviewed them or suggested modifications. Ultimately, Linus Torvalds would accept those patches and integrate them into the kernel. This approach worked effectively in the beginning. The project was relatively small, and the number of contributors was limited, so human judgment and communication were sufficient to manage the process.</p><p>However, as Linux grew rapidly, the situation changed completely. A large number of developers began submitting patches simultaneously, and conflicting changes became increasingly frequent. Determining which changes should be applied first and which modifications were more appropriate became a complex process. Moreover, all of this responsibility was concentrated on a single individual, Linus Torvalds.</p><p>This was not merely a matter of being “busy.” Tracking code changes became increasingly difficult, and the likelihood of mistakenly integrating incorrect patches grew higher. In a situation where multiple developers were working simultaneously, failing to systematically manage change history could jeopardize the stability of the entire project.</p><p>The more fundamental issue was that this structure was not scalable. The Linux kernel continued to grow, but the collaboration model could not keep pace. As the number of developers increased, inefficiency also increased. Eventually, this problem reached a point where it could no longer be ignored.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-1.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-1.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-1.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-1.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>At this point, the Linux development team had to make a decision. Should they continue with the existing approach and accept increasing chaos, or should they introduce new tools to fundamentally change how collaboration worked? They ultimately chose the latter. And that decision led to the adoption of BitKeeper.</p><p>However, this choice would soon give rise to another problem.</p><h2 id="the-adoption-of-bitkeeper-and-an-uneasy-coexistence">The Adoption of BitKeeper and an Uneasy Coexistence</h2><p>The decision by the Linux kernel development team to adopt BitKeeper was not simply a matter of replacing one tool with another. It was an attempt to transform the very nature of collaboration. BitKeeper was, by the standards of the time, a highly advanced version control system. It provided efficient tracking of code changes and supported distributed collaboration to a certain extent, offering a level of capability that clearly surpassed existing tools.</p><p>With BitKeeper in use, Linux kernel development began to take on a much more structured form. Tracking changes became easier, and multiple developers could work simultaneously while managing conflicts more effectively. Most importantly, the burden that had previously been concentrated on Linus Torvalds began to be partially distributed. At this stage, BitKeeper was no longer just a tool—it had become a core piece of infrastructure that made Linux development possible.</p><p>However, this transformation came with a clear limitation. BitKeeper was not open source software; it was a proprietary product developed by a company called BitMover. Although it was provided free of charge to Linux kernel developers, the terms of its usage could be changed at any time. In other words, the world’s largest open source project had become <strong>dependent on the decisions of an external company</strong>.</p><p>This situation gradually created discomfort within the open source community. Technically, BitKeeper was an extremely useful tool, but philosophically, it was difficult to accept. The fact that an open source project depended on proprietary software felt contradictory. Some developers criticized the use of BitKeeper, and attempts were made to find alternative solutions.</p><p>This tension persisted for some time, but eventually, it collapsed due to a single incident. And that incident would not only lead to a tool replacement, but would also mark the beginning of a new era.</p><h2 id="the-bitkeeper-incident-%E2%80%94-the-moment-everything-collapsed">The BitKeeper Incident — The Moment Everything Collapsed</h2><p>BitKeeper had stabilized Linux kernel development to a certain extent, but at the same time, it was an unstable foundation that could collapse at any moment. That instability had long existed beneath the surface, but it eventually emerged through a single incident. This was not merely a technical issue or a licensing dispute. It was the moment when the <strong>fundamental tension between open source and proprietary software exploded into the open</strong>.</p><p>In 2005, some developers attempted to analyze BitKeeper’s internal protocols. This effort was not simply driven by curiosity, but was part of a broader attempt to break away from dependence on BitKeeper. However, BitMover regarded this as a clear violation of its license. As a result, BitMover decided to revoke the free usage rights it had previously granted to Linux kernel developers.</p><p>This decision was not just a matter of “losing access to a tool.” The entire collaboration infrastructure on which Linux kernel development depended disappeared overnight. In a project involving thousands of developers, the loss of a version control system effectively meant that development itself could come to a halt. This incident represented an <strong>existential crisis</strong> for the Linux kernel development team.</p><p>More importantly, this was not just an external shock. The structure in which an open source project depended on a proprietary tool was inherently fragile, and the same problem could occur again at any time. The BitKeeper incident was the moment that risk became reality. And from that moment, the Linux kernel development team reached a clear conclusion: <strong>they could no longer depend on external tools</strong>.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-2.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-2.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-2.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-2.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>This incident was not simply the disappearance of a tool; it was the moment when a new tool became absolutely necessary. And this necessity was not something that could be solved externally. The Linux kernel development team found themselves in a position where they had to <strong>build their own tool</strong>.</p><p>At this point, the question was no longer “which tool should we use.”<br>The question had become <strong>“how do we create a tool that can solve this problem?”</strong></p><h2 id="linus-torvalds-builds-the-tool-himself">Linus Torvalds Builds the Tool Himself</h2><p>After the BitKeeper incident, Linus Torvalds made a swift decision. Instead of searching for a replacement tool, he chose to <strong>build one himself</strong>. This decision was not just a technical response; it was closer to a philosophical shift. It was a declaration that they would no longer rely on external tools, but instead construct a system tailored to their own needs.</p><p>When designing Git, Linus Torvalds established several clear principles. The most important was speed. The Linux kernel had a massive codebase, with a high volume of rapid changes. If the version control system were slow, development itself would inevitably become a bottleneck. Therefore, Git was designed from the beginning as a system that prioritized <strong>extreme performance</strong>.</p><p>Another crucial principle was data integrity. Git adopted a structure in which all changes are managed using SHA hashes. This was not merely an implementation detail, but a core design choice to ensure system reliability. It structurally prevents corruption or tampering of code history. This design would later play a key role in establishing Git as a trustworthy system.</p><p>The most significant shift, however, was the distributed architecture. Linus recognized that centralized systems fundamentally suffered from scalability issues. As a result, Git was designed so that every developer could have a complete copy of the repository locally. This was not just a technical difference—it was a decision that fundamentally changed how collaboration works.</p><p>Remarkably, the initial version of Git was created in just a few weeks. This fact may sound exaggerated, but Git was indeed developed at an astonishing pace. Of course, early Git was far from polished and not particularly user-friendly. But what mattered was not perfection—it was direction. Git was a tool designed to solve a specific problem, and its architecture was precisely aligned with that purpose.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-3.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-3.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-3.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-3.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>At this stage, Git was not yet a world-changing tool. It was simply a tool created to restore Linux kernel development. However, its design was fundamentally different from existing systems. And that difference soon began to trigger much larger changes.</p><p>Now the important question becomes this:<br>What exactly made Git <strong>different from existing version control systems</strong>?</p><h2 id="the-core-structure-of-git-%E2%80%94-what-made-it-different">The Core Structure of Git — What Made It Different</h2><p>The reason Git was fundamentally different from existing version control systems was not just a matter of features, but that <strong>it approached data in an entirely different way</strong>. Most traditional systems stored changes as “diffs”—that is, they recorded the differences between versions and built history from those differences.</p><p>Git, however, chose a completely different approach. Instead of storing differences, Git stores changes as <strong>snapshots</strong>. It records the entire state of the project at a given point in time and constructs history from these snapshots. At first glance, this approach might seem inefficient, but in practice it creates a system that is both extremely fast and highly reliable. Git avoids redundant storage by reusing unchanged data, maintaining efficiency even with a snapshot-based model.</p><p>Another critical aspect of Git is its use of hashes to identify all objects. Commits, files, and directories all have unique hash values determined by their content. This is not just an identification mechanism—it is a fundamental system for ensuring data integrity. If any data is altered, its hash changes, and the system can immediately detect it.</p><p>Git also introduced a radically different approach to branching. In traditional systems, creating a branch was expensive and complex. In Git, however, branches are essentially lightweight pointers, making them extremely fast to create. This difference had a profound impact on how developers work. Developers no longer feared branching and could experiment freely without risk.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-4.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-4.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-4.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-4.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>These structural differences did more than improve performance—they transformed how developers think. Git was no longer just a tool for managing files; it became a system for managing <strong>the flow of changes</strong>. And this shift would go on to deeply influence collaboration practices and the broader development culture.</p><p>At this point, Git was beginning to evolve beyond a tool for the Linux kernel—it was becoming the starting point of a new development paradigm.</p><h2 id="the-paradigm-shift-of-distributed-version-control">The Paradigm Shift of Distributed Version Control</h2><p>The most fundamental difference that set Git apart from existing version control systems was not its features, but its structure. At the core of that structure was the concept of <strong>Distributed Version Control Systems (DVCS)</strong>. In traditional centralized systems, a single server acted as the source of truth, and developers performed their work by depending on that server. Every process—pulling code, making changes, and pushing updates—was centered around a central authority. While this model was simple and easy to understand, it was built upon a critical assumption: <strong>all work must be connected to a central system</strong>.</p><p>Git completely overturned this assumption. In Git, every developer has a full copy of the repository locally, including its entire history, and can perform all operations independently within their own environment. This structural shift may seem like a technical detail at first glance, but in practice, it fundamentally changed how developers interact with code. Developers can create commits, branch, and rewrite history without any network connection. In other words, they are no longer dependent on the state of a central system to make progress.</p><p>This change had a particularly profound impact on collaboration. In centralized systems, minimizing conflicts required coordinating work and carefully managing who changed what and when. In contrast, Git enabled developers to work independently and merge their changes later when needed. This transformed collaboration from a synchronization-centric model into a merge-centric model. Developers no longer needed to wait for one another, enabling parallel development at a scale that was previously difficult to achieve. This became especially powerful in large-scale projects involving many contributors.</p><p>Additionally, this structure significantly improved system resilience. Even if a central server failed, every developer’s local repository still contained the full project history. The system no longer relied on a single point of failure. This characteristic elevated Git beyond a mere development tool and positioned it as a <strong>collaboration infrastructure grounded in distributed system principles</strong>.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-5.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-5.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-5.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-5.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>At this point, Git was no longer just a tool for the Linux kernel. It had become a new model that structurally solved the limitations of previous development practices. As this model began to spread to more projects, it gradually established itself as a new standard.</p><p>However, a change in structure alone does not immediately transform development culture. That transformation emerges gradually, as new practices and workflows build on top of the tool.</p><h2 id="the-new-collaboration-model-created-by-git">The New Collaboration Model Created by Git</h2><p>The impact of Git extended far beyond its technical architecture. The real transformation emerged in the <strong>new collaboration model</strong> that formed around it. Git made branching extremely lightweight, and this alone fundamentally changed how developers approached their work. In earlier systems, creating branches was costly and complex, which discouraged their use. As a result, most development occurred on a single main line. With Git, however, creating a branch became nearly effortless, allowing developers to experiment freely without fear.</p><p>This shift naturally led to the concept of feature-based development. Developers began creating separate branches for individual features, working independently within those branches, and then merging them back into the main branch after sufficient review. This process organically integrated code review into the workflow, transforming collaboration from simple code sharing into a <strong>decision-making process</strong>. Code was no longer just written and shared—it was discussed, evaluated, and refined collectively.</p><p>Perhaps the most important change was in how merging itself was perceived. Previously, the goal was to avoid conflicts as much as possible. With Git, merging became an intentional and meaningful process. It required developers to actively decide how different pieces of work should be combined and which changes should be accepted. This elevated merging from a technical necessity to a central part of collaboration, reflecting the team’s standards and philosophy.</p><p>These collaboration patterns became even more defined with the emergence of platforms like GitHub. The concept of a Pull Request was not merely a request to merge code, but an interface for discussion, review, and shared decision-making. What Git ultimately created was not just a version control system, but a <strong>framework for developers to think, communicate, and make decisions together</strong>.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-6.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-6.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-6.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-6.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>At this stage, Git could no longer be fully described as just a tool. It had become a system that defined how collaboration worked, influenced developer behavior, and shaped overall team productivity. This naturally leads to the next question.</p><p>How did such a powerful system become the global standard?</p><h2 id="how-git-became-the-standard">How Git Became the Standard</h2><p>When Git was first introduced, it was a specialized tool built for the Linux kernel. It was not particularly user-friendly, and many of its concepts were unfamiliar to developers accustomed to existing systems. However, over time, Git began to spread across more projects, eventually becoming the standard used in nearly all development environments. This transformation cannot be explained by technical superiority alone.</p><p>One of the most critical factors was the expansion of the <strong>ecosystem built around Git</strong>. While Git itself was powerful, it was not enough on its own to transform development culture. The decisive turning point came with platforms like GitHub. GitHub, built on top of Git, made collaboration more accessible and exposed open-source projects to a much wider audience. It transformed Git from a purely technical choice into a <strong>social and cultural choice</strong>.</p><p>Git also integrated seamlessly with a wide range of development tools and environments. IDEs, CI/CD systems, and cloud platforms all began to center their workflows around Git. Choosing Git was no longer just about selecting a version control system—it became a foundational decision that shaped the entire development environment. As more projects adopted Git, it gradually became not just a popular choice, but a <strong>de facto standard</strong>.</p><p>An important aspect of this transition is that Git was not imposed as a standard. No single organization or authority mandated its use. Instead, it became the standard organically, as developers independently chose it based on its ability to solve real problems. This is what made Git’s dominance particularly significant—it was not enforced, but earned through widespread adoption.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-7.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-7.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-7.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-7.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>At this point, Git is no longer just a tool tied to a specific project. It has become the foundation of modern software development and the standard by which collaboration is defined. And this trajectory does not stop here.</p><p>The collaboration model introduced by Git continues to expand through platforms like GitHub, ultimately connecting the entire open-source ecosystem into a single, interconnected network.</p><h2 id="a-tool-born-from-crisis-one-that-changed-culture">A Tool Born from Crisis, One That Changed Culture</h2><p>When we trace Git’s story back to the beginning, its essence starts from a surprisingly simple point. Git was not a system created out of some grand vision or long-term strategic plan. It was merely a tool built to solve a single crisis—the very real problem of losing access to BitKeeper. Yet this simple origin gave Git a uniquely clear direction. It was not a tool designed to implement abstract ideals, but rather a system engineered to solve <strong>real, immediate problems</strong>.</p><p>This point is crucial in understanding how Git evolved. Git was never intended from the outset to be a universal tool for all developers. It was built specifically to support the extremely complex environment of Linux kernel development. That is why performance, integrity, and scalability were not optional features, but essential design requirements. And as a result, those design choices became a strong foundation that could be applied to virtually any other project. In other words, although Git was created to solve a specific problem, the way it solved that problem contained a <strong>universally applicable structure for problem-solving</strong>.</p><p>Git’s true impact, however, reveals itself beyond the technical domain. Git did not simply provide a tool for managing code; it redefined how developers collaborate. Before Git, collaboration was largely about avoiding conflicts and cautiously managing changes. After Git, collaboration became about actively creating branches, isolating changes, and integrating them through merges. This shift was not merely an improvement in efficiency—it represented a <strong>fundamental change in how developers perceive and work with code</strong>.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-8.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-8.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-8.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-8.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>This transformation unfolded gradually, but it ultimately led to a clear outcome. Git was no longer just one of many tools—it became a fundamental prerequisite for development. Starting a new project effectively meant creating a Git repository, and collaborating meant working within a Git-centered workflow. In this sense, Git evolved beyond being a tool and became a <strong>precondition of the development environment itself</strong>.</p><p>What is particularly interesting is that this shift was not enforced by any central authority or standardization body. Git spread organically, becoming the standard through the collective choices of developers. This was not simply because Git was technically superior, but because it addressed real problems encountered in everyday development. In the end, Git did not survive because it was merely a “good tool,” but because it was a <strong>necessary tool</strong>.</p><p>And this is not where the story ends. The transformation initiated by Git was not a complete conclusion, but rather the beginning of a new phase. The distributed structure and collaborative model introduced by Git would soon expand further through platforms like GitHub, ultimately connecting the entire developer ecosystem into a unified network.</p><p>In that sense, Git was not the end—it was the beginning. And the next chapter of this story explores how a platform was built on top of Git, and how open source evolved into a fully connected ecosystem.</p>]]></content:encoded>
                </item>
                <item>
                    <title>Why You Don’t Understand 2&gt;&amp;1 — A Sign You Misunderstand Linux I/O</title>
                    <link>https://en-signal.ceak.dev/linux-io-2-and-1-explained-file-descriptor-redirection/</link>
                    <guid isPermaLink="true">https://en-signal.ceak.dev/linux-io-2-and-1-explained-file-descriptor-redirection/</guid>
                    <pubDate>Thu, 09 Apr 2026 22:03:07 +0900</pubDate>
                    <dc:creator><![CDATA[ceak]]></dc:creator>
                    <category><![CDATA[🎯Docs]]></category><category><![CDATA[📚 Linux I/O and Data Flow — From Streams to Pipelines]]></category><category><![CDATA[Linux]]></category><category><![CDATA[Shell]]></category><category><![CDATA[System Internals]]></category>
                    <description><![CDATA[If you think 2&gt;&amp;1 “merges stderr into stdout,” you misunderstand Linux I/O. This article explains how file descriptors, redirection, and pipes actually work — and why execution order changes everything.]]></description>
                    <content:encoded><![CDATA[<h2 id="why-21-is-always-confusing-%E2%80%94-the-starting-point-of-a-syntax-used-without-understanding">Why <code>2&gt;&amp;1</code> Is Always Confusing — The Starting Point of a Syntax Used Without Understanding</h2><p>Most developers have used <code>2&gt;&amp;1</code> at least once, but very few can actually explain it. In many cases, this syntax is remembered as “combining errors into output.” However, this explanation only describes the result and does not explain why it behaves that way. As a result, the moment the same syntax produces different outcomes depending on the situation, understanding immediately collapses. The reason to read this document is simple. It is to understand <code>2&gt;&amp;1</code> instead of memorizing it.</p><p>The problem is that this syntax is not a simple string, but an expression that directly manipulates the Linux I/O structure internally. In other words, <code>2&gt;&amp;1</code> does not change the output result, but reconstructs the path that the output follows. If this distinction is not understood, it becomes impossible to explain why the result changes depending on the execution order of commands. For example, the following two commands appear very similar on the surface, but they produce completely different results in reality.</p><pre><code class="language-bash">command &gt; file 2&gt;&amp;1
command 2&gt;&amp;1 &gt; file
</code></pre><p>In the first command, stdout is redirected to the file first, and then stderr follows that location. In the second command, stderr follows the existing stdout first, and then only stdout is redirected to the file. This difference is not a matter of syntax, but a matter of “when and what is referenced.” In other words, <code>2&gt;&amp;1</code> is not a syntax that produces a fixed result, but a dynamic structure that changes depending on the execution timing. This is where most confusion arises.</p><p>To solve this problem, it is not enough to explain <code>2&gt;&amp;1</code> itself. The underlying structure beneath it must be understood. A simple explanation like “combining outputs” cannot explain this behavior. To understand why this syntax behaves this way, we must first address the fact that we have misunderstood the concept of output itself. The next step is to break down that misunderstanding.</p><h2 id="why-we-learn-21-incorrectly-%E2%80%94-because-it-is-learned-as-a-pattern-not-a-concept">Why We Learn <code>2&gt;&amp;1</code> Incorrectly — Because It Is Learned as a Pattern, Not a Concept</h2><p>Most developers first encounter Linux I/O not through concepts, but through usage patterns. That is, they receive a rough explanation of what stdout and stderr are, and then <code>2&gt;&amp;1</code> is presented as something that “works when used like this.” This approach helps in quickly obtaining results, but it does not help in understanding the structure. As a result, <code>2&gt;&amp;1</code> is remembered as a syntax pattern, and its meaning remains detached from its actual behavior.</p><p>The problem with this learning method becomes apparent in edge cases. It works fine in basic usage, but as soon as conditions change slightly, the result becomes unpredictable. For example, when used with pipes or when the order of redirection changes, most developers cannot intuitively explain the result. This is not because they do not know the syntax, but because they do not understand the structure on which the syntax operates. In other words, we know how to use <code>2&gt;&amp;1</code>, but we do not know why it behaves that way.</p><p>This issue is not simply a matter of learning style, but arises from learning concepts in isolation. stdout, stderr, pipes, and redirection are explained separately, but in reality, they operate within a single system. However, because this connection is omitted during learning, each concept is perceived as independent. As a result, when multiple concepts operate simultaneously, such as in <code>2&gt;&amp;1</code>, understanding breaks down. The essence of the problem is not that the syntax is difficult, but that the concepts are not connected.</p><p>Now, this connection must be restored. To do that, we must first redefine what output actually is. We need to understand precisely what we think output is, and how it truly operates. The next step addresses exactly this point.</p><h2 id="the-core-problem-%E2%80%94-we-misunderstand-%E2%80%9Coutput%E2%80%9D-itself">The Core Problem — We Misunderstand “Output” Itself</h2><p>Most developers think that output goes to the screen. That is, when a program runs, the result is displayed in the terminal, and that is considered the entirety of output. However, this understanding is not sufficient to explain the Linux I/O structure. Output is not fixed to a specific destination, but is merely a path through which data flows. In other words, output is not about “going somewhere,” but about “being able to go anywhere.”</p><p>In Linux, output is divided into two streams: <code>stdout</code> and <code>stderr</code>. These streams are not fixed to a specific device, but can be connected to various targets such as files, terminals, or pipes. A program simply writes data to these streams, and where that data is delivered is determined by a separate structure. If this structure is not understood, one may assume that output always behaves in the same way. And this assumption is precisely what causes the failure to understand <code>2&gt;&amp;1</code>.</p><p>At this point, an important shift is required. Output is not a result, but a flow. In other words, a program does not “produce” results, but “emits” data. From this perspective, stdout and stderr are not merely output channels, but paths through which data travels. And these paths can be changed at any time. Redirection changes these paths, and pipes connect these paths to other programs. In other words, everything operates on top of flow.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-65.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-65.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-65.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-65.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>At this point, the definition of output has changed. Output is no longer the result displayed on the screen, but a flow of data moving within the system. Once this perspective is established, it becomes possible to understand the next step, the file descriptor structure. And the moment that structure is understood, <code>2&gt;&amp;1</code> is no longer a syntax to memorize, but a behavior that can be naturally explained.</p><h2 id="rebuilding-the-linux-io-model-%E2%80%94-the-relationship-between-stdin-stdout-and-stderr">Rebuilding the Linux I/O Model — The Relationship Between stdin, stdout, and stderr</h2><p>To understand <code>2&gt;&amp;1</code>, there is a prerequisite that must be clearly established. stdout and stderr are not merely different “types of output.” They are structurally independent paths within the system. In many cases, stdout is explained as standard output and stderr as error output, but this distinction is functional rather than structural. In reality, both outputs are managed separately through distinct file descriptors. Without understanding this structure, it is impossible to explain how redirection and <code>2&gt;&amp;1</code> actually work.</p><p>In Linux, all input and output are managed through numerical identifiers called file descriptors. stdin is file descriptor 0, stdout is 1, and stderr is 2. These numbers are not just labels. Each represents an independent data channel. When a program writes to stdout, it is internally performing a write operation on file descriptor 1. Similarly, stderr outputs are written through file descriptor 2. This means both outputs follow the same mechanism, but they are transmitted through completely separate paths.</p><p>The critical point here is that these file descriptors are not permanently bound to a specific destination. By default, stdout and stderr point to the terminal, but through redirection, they can be reassigned to files or other targets. This means output does not follow a fixed path. It is dynamically determined at execution time based on how the process is configured. This behavior is external to the program’s logic. As a result, the same program can produce entirely different output behaviors depending on how it is executed.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-66.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-66.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-66.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-66.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>▶ To understand this concept more deeply: <a href="https://en-signal.ceak.dev/linux-stdout-stderr-redirection/" rel="noreferrer">Understanding Linux I/O Streams — stdin, stdout, stderr, and Redirection</a></p><p>Now it is clear that stdout and stderr are independent paths. The next step is to understand how these paths are actually reassigned and connected in practice. At that point, it becomes evident that redirection is not simply about changing output, but about restructuring the flow itself.</p><h2 id="the-critical-shift-%E2%80%94-21-does-not-%E2%80%9Cmerge%E2%80%9D-it-reassigns-references">The Critical Shift — <code>2&gt;&amp;1</code> Does Not “Merge,” It Reassigns References</h2><p>Explaining <code>2&gt;&amp;1</code> as “merging stderr into stdout” is fundamentally insufficient. That description only captures the result, not the internal behavior. The accurate interpretation is that stderr does not take on the same “value” as stdout. Instead, it is configured to reference the same target that stdout currently points to. This distinction is essential. Copying a value and sharing a reference are completely different operations.</p><p>In Linux, a file descriptor represents a reference to a specific target. If stdout points to a file, then file descriptor 1 maintains a reference to that file. When <code>2&gt;&amp;1</code> is applied, stderr is reassigned to reference the same target that stdout is currently pointing to. This means stderr does not dynamically follow future changes to stdout. Instead, it captures the target at that exact moment and shares it. After this reassignment, both descriptors operate independently, even though they reference the same destination.</p><p>This explains why <code>2&gt;&amp;1</code> is highly sensitive to execution order. File descriptors are configured sequentially during command parsing. The timing of when a reference is established determines the final behavior. Therefore, <code>2&gt;&amp;1</code> is not a static transformation. It is a context-dependent operation that binds stderr to the current state of stdout at a specific point in execution.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-67.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-67.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-67.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-67.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>At this point, the essence of <code>2&gt;&amp;1</code> is clear. It does not combine data streams. It aligns their underlying paths. The next step is to examine how this behavior changes depending on command execution order. Only then can we fully explain why identical syntax can produce different results.</p><h2 id="why-order-changes-the-result-%E2%80%94-the-problem-of-execution-timing">Why Order Changes the Result — The Problem of Execution Timing</h2><p>The primary reason many developers fail to understand <code>2&gt;&amp;1</code> is that they cannot explain how execution order affects the outcome. The two commands examined earlier use the same elements, yet produce different results because the order of operations changes. This difference is not syntactic. It is rooted in when each file descriptor establishes its reference. The Linux shell processes redirection and file descriptor manipulation strictly from left to right. This sequence determines the final state.</p><pre><code class="language-bash">command &gt; file 2&gt;&amp;1
</code></pre><p>In this case, stdout is first redirected to a file. This means file descriptor 1 now points to the file. Then <code>2&gt;&amp;1</code> is executed, causing stderr to reference whatever stdout currently points to, which is the file. As a result, both stdout and stderr are written to the file. Now consider the following command.</p><pre><code class="language-bash">command 2&gt;&amp;1 &gt; file
</code></pre><p>Here, stderr is first reassigned to follow stdout. At this moment, stdout still points to the terminal. Therefore, stderr becomes linked to the terminal. After that, stdout is redirected to a file. This change only affects file descriptor 1. stderr remains connected to the terminal because its reference was already established earlier. The result is that stdout is written to the file, while stderr is still printed to the terminal.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-68.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-68.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-68.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-68.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>Understanding this structure removes any ambiguity around <code>2&gt;&amp;1</code>. It is not an exceptional rule. It is simply the natural consequence of how file descriptors are assigned in sequence. Linux I/O operates as a state-based system, where each step defines the context for the next. Once this perspective is established, the same principles can be applied to pipes and more complex redirection scenarios.</p><h2 id="what-changes-when-pipe-meets-21-%E2%80%94-separation-and-composition-of-data-flow">What changes when pipe meets 2&gt;&amp;1 — separation and composition of data flow</h2><p>Understanding <code>2&gt;&amp;1</code> does not automatically mean you understand pipe. In many cases, confusion actually begins again when pipe is introduced. The reason is that pipe is often understood only as “passing output to the next command.” In reality, pipe operates only on stdout. stderr does not pass through the pipe by default. If this fact is not understood, it is impossible to explain why <code>2&gt;&amp;1</code> must be positioned before the pipe.</p><p>A pipe creates a new connection between two processes. This connection is implemented at the file descriptor level. The stdout of the first command is connected to the stdin of the second command. At this point, stderr is not affected at all. In other words, pipe reconfigures only the stdout path, while stderr maintains its existing path. Because of this structure, error messages are still printed to the terminal, while only normal output is passed to the next command.</p><pre><code class="language-bash">command | grep something
</code></pre><p>In this command, grep receives only the stdout of command as input. stderr is still printed to the terminal. Therefore, error messages do not pass through grep. This behavior may not feel intuitive, but it is very clear from the file descriptor perspective. The pipe connects fd1 only, and leaves fd2 unchanged. Because of this, if you want stderr to pass through the pipe, an additional operation is required.</p><p>This is where <code>2&gt;&amp;1</code> comes in. If stderr is connected to the same target as stdout, then the pipe will effectively receive both outputs. The important point is the order. In <code>command 2&gt;&amp;1 | grep something</code>, stderr is first connected to stdout, and then the pipe connects stdout. As a result, both outputs are passed to grep. In contrast, <code>command | grep something 2&gt;&amp;1</code> behaves completely differently. In this case, <code>2&gt;&amp;1</code> is processed inside grep and has no effect on the stderr of command.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-70.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-70.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-70.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-70.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>Now the relationship between pipe and <code>2&gt;&amp;1</code> is no longer a simple combination, but a question of how to merge different paths. In the next step, this structure is examined in real-world usage, where its role shifts from syntax to a design tool.</p><h2 id="why-21-is-used-in-practice-%E2%80%94-logging-debugging-and-reproducibility">Why 2&gt;&amp;1 is used in practice — logging, debugging, and reproducibility</h2><p><code>2&gt;&amp;1</code> is not just syntax. It is a tool for controlling execution results. In practice, the flow of output often matters more than the content of output. This is especially true in logging, failure analysis, and batch processing. In these cases, whether stdout and stderr are separated or combined directly affects how results are interpreted. Here, <code>2&gt;&amp;1</code> is used as a mechanism to explicitly control output paths.</p><p>A representative example is log file generation. If only stdout is redirected to a file while stderr remains in the terminal, the log becomes incomplete. If only stderr is redirected, the normal execution flow cannot be observed. Therefore, in most cases, both outputs are combined into a single file. The common pattern used here is <code>&gt; file 2&gt;&amp;1</code>. This command sends stdout to a file and connects stderr to the same file. As a result, all output is recorded in a single log.</p><pre><code class="language-bash">command &gt; app.log 2&gt;&amp;1
</code></pre><p>This structure is not just for convenience. It is for ensuring reproducibility. If the same input does not produce a consistent log, it becomes impossible to reproduce problems. If stderr is separated, the log alone cannot fully reconstruct the situation. Therefore, controlling output paths is essential in practice. <code>2&gt;&amp;1</code> is the most fundamental tool that makes this control possible.</p><p>Another important use case is debugging. All outputs of a command can be passed to another program for analysis. For example, if you want to filter including error messages using grep, stderr must be merged into stdout. In this case, a pattern such as <code>command 2&gt;&amp;1 | grep error</code> is used. This pattern is not just about string matching. It turns the entire execution flow into an analyzable stream.</p><p>▶ To understand this concept more deeply: <a href="https://en-signal.ceak.dev/linux-pipe-command-explained-with-examples/" rel="noreferrer">What Is a Pipe (|)? A Clear Guide to the Linux Pipe Concept</a></p><p>In this way, <code>2&gt;&amp;1</code> is not just syntax. It is used as a tool for designing output flow. In the next step, the focus shifts to why this design is frequently misunderstood, and where incorrect mental models originate.</p><h2 id="why-most-explanations-are-wrong-%E2%80%94-the-misleading-model-of-%E2%80%9Cmerging%E2%80%9D">Why most explanations are wrong — the misleading model of “merging”</h2><p>The primary reason developers fail to understand <code>2&gt;&amp;1</code> is that most explanations are result-oriented. Saying “stderr is merged into stdout” is intuitive, but it does not explain the structure. This expression only describes the visible result, and does not reveal what actually happens internally. Because of this, users cannot explain why the same command produces different results depending on order.</p><p>The core problem is that the term “merge” encourages a data-centric mental model. It leads to the misunderstanding that two outputs are combined into a single stream. In reality, no data is merged. Instead, the path is reconfigured. stderr simply references the same target as stdout. Without understanding this distinction, it is impossible to explain why identical syntax can produce different results.</p><p>Additionally, many resources omit the concept of file descriptors. This is often done to simplify explanations, but it ultimately prevents structural understanding. Without understanding file descriptors, redirection appears to be just “changing output.” In reality, it is <strong>reassigning output paths</strong>. This distinction is critical. Only by understanding paths can you explain pipe interaction, execution order, and reference behavior as a unified system.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-73.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-73.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-73.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-73.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>▶ To understand this concept more deeply: <a href="https://en-signal.ceak.dev/grep-sort-uniq-pipeline-why/" rel="noreferrer">Why grep, sort, and uniq Are Piped Together — The Core Pattern for Log Analysis</a></p><p>Ultimately, understanding <code>2&gt;&amp;1</code> is not about learning a single piece of syntax. It is about understanding the entire Linux I/O system structurally. Once this perspective is established, all redirection and pipe behavior can be interpreted through the same underlying principles. The next step is to connect this understanding into a unified system-level flow.</p><h2 id="what-changes-when-you-understand-linux-io-structurally-%E2%80%94-it-becomes-a-predictable-system">What Changes When You Understand Linux I/O Structurally — It Becomes a Predictable System</h2><p>If you understand <code>2&gt;&amp;1</code>, one important change occurs. You no longer need to memorize commands. Most developers treat redirection and pipes as “patterns to remember.” However, this approach breaks as soon as the situation changes slightly. The reason is that patterns are memorized, but the structure is not understood. In contrast, if you understand file descriptors and reference structures, you can predict results even in unfamiliar situations.</p><p>The core of this shift is the ability to always trace where output is going. stdout and stderr each have independent paths. These paths can be modified through redirection. The expression <code>2&gt;&amp;1</code> makes these paths shared. A pipe connects only the stdout path to the next process. By combining these three principles, you can determine the result of any command. What matters is not remembering the outcome, but tracing what each file descriptor points to at each step.</p><p>When encountering a complex command, you no longer ask “will this work.” Instead, you check where stdout is pointing. Then you check when stderr referenced stdout. With just these two questions, you can reconstruct the entire flow. This approach does not simply improve command usage. It changes how you interpret system behavior itself. Linux I/O is not something to memorize. It is something to understand as a state-based system.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-71.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-71.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-71.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-71.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>This perspective does not apply only to shell commands. Internally, every program operates on the same file descriptor model. Understanding this structure allows you to interpret system logs, inter-process communication, and stream-based processing in the same way. The next step is to extend this understanding into broader system design concepts.</p><h2 id="extended-concept-%E2%80%94-file-descriptors-are-not-just-output-but-a-system-interface">Extended Concept — File Descriptors Are Not Just Output, but a System Interface</h2><p>When Linux I/O is understood structurally, it becomes clear that file descriptors are not just a means of input and output. They are an interface between a process and the outside world. stdout and stderr are only a subset of this model. In reality, file descriptors can connect to files, sockets, pipes, and devices. The same mechanism is used to interact with completely different system components.</p><p>The key idea is the Linux design principle that <strong>everything is treated like a file</strong>. A program does not need to know what the target is. It only performs a write operation on a file descriptor. The target may be a file, a network socket, or another process. This abstraction is what makes pipes and redirection work naturally. The expression <code>2&gt;&amp;1</code> is also built on this same abstraction. It is not a special feature. It is part of the system’s core design.</p><p>From this perspective, <code>2&gt;&amp;1</code> is not just syntax. It is an operation that reconfigures interface connections. When stderr shares the same target as stdout, it means both interfaces are connected to the same destination. This connection becomes a way to control data flow. For example, logs can be sent not only to files, but also over the network or to other processes. All of this is implemented using the same mechanism. File descriptors are the fundamental units that connect system components.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-72.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-72.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-72.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-72.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>▶ To understand this concept more deeply: <a href="https://en-signal.ceak.dev/linux-pipe-command-explained-with-examples/" rel="noreferrer">What Is a Pipe (|)? A Clear Guide to the Linux Pipe Concept</a></p><p>At this point, <code>2&gt;&amp;1</code> is no longer an isolated concept. It is one example that naturally emerges from understanding the entire Linux I/O system. With this perspective, any new command or situation can be interpreted using the same principles. What matters is not the syntax itself, but the system structure in which that syntax exists.</p>]]></content:encoded>
                </item>
                <item>
                    <title>Readium Devlog #1 — Starting from a Misdefined Problem</title>
                    <link>https://en-signal.ceak.dev/readium-wrong-problem-definition/</link>
                    <guid isPermaLink="true">https://en-signal.ceak.dev/readium-wrong-problem-definition/</guid>
                    <pubDate>Wed, 08 Apr 2026 19:28:28 +0900</pubDate>
                    <dc:creator><![CDATA[ceak]]></dc:creator>
                    <category><![CDATA[📱 Apps]]></category><category><![CDATA[📚 Readium Development Log — Why Design Keeps Failing]]></category><category><![CDATA[Software Architecture]]></category><category><![CDATA[System Design]]></category><category><![CDATA[Local First]]></category>
                    <description><![CDATA[I thought the problem was the lack of records.
In reality, I didn’t know how to define what a record is.
This post traces the first crack in a system built on that misunderstanding.]]></description>
                    <content:encoded><![CDATA[<h2 id="i-never-intended-to-build-an-app">I Never Intended to Build an App</h2><p>I never started with the intention of building an app. There was no plan to target a specific market, nor any ambition to grow a service. It wasn’t even a side project driven by technical curiosity. In fact, it was closer to the opposite. While reading a book without any particular goal in mind, I encountered a very small inconvenience. It wasn’t significant, and it wasn’t something that demanded an immediate solution. But for some reason, that feeling didn’t go away. Once I noticed it, I kept encountering the same friction every time I read.</p><p>What matters here is not the size of the inconvenience, but its persistence. Most inconveniences fade over time. This one didn’t. It didn’t end when I finished reading; instead, it became more noticeable afterward. There was always something missing after closing the book. That sense of absence repeated itself and eventually solidified into a single question. What exactly am I missing? This question didn’t immediately translate into a functional requirement. It remained vague. So instead of trying to solve it, I simply observed it.</p><p>At this point, a critical assumption had already formed. The assumption that “something is missing.” The problem is that this kind of feeling easily turns into certainty without a clear definition. I accepted the discomfort as a problem and assumed it needed to be solved. But at that time, I couldn’t clearly explain what that problem actually was. It only felt real because it kept recurring. Every decision that followed originated from this undefined starting point. The attempt to solve the problem began, but the problem itself remained unstructured.</p><p>So the starting point of this story is not “I decided to build an app.” It is the opposite. I started without a clear plan, without a properly defined problem. And this state becomes the foundation for every design decision that follows. Looking back, this moment was the most critical point. Because every subsequent decision was built on top of this vague definition. This series begins precisely from that point.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-84.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-84.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-84.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-84.png 1536w" sizes="(min-width: 720px) 720px"></figure><h2 id="the-nature-of-the-discomfort-%E2%80%94-the-misconception-of-%E2%80%9Cno-records%E2%80%9D">The Nature of the Discomfort — The Misconception of “No Records”</h2><p>When you keep tracing the discomfort, it eventually condenses into a single sentence. “There is no record.” While reading, time clearly passes, and actions accumulate. But the moment the book is closed, everything seems to disappear. How long I read today, when I stopped, how long I paused in between—these all become dependent on memory. And memory fades quickly. So I naturally reached a conclusion. The discomfort comes from the absence of records. This judgment seems intuitively correct.</p><p>However, there is a critical misconception hidden in this conclusion. I thought “there are no records,” but in reality, records were continuously being generated. The moment reading starts, the moment it stops, the amount read, the reason for interruption—these are all elements that can be expressed as data. The issue was not that data didn’t exist, but that it was not structured. In other words, the problem was not the absence of records, but the absence of a defined structure for those records. This distinction may appear subtle, but it leads to fundamentally different design outcomes.</p><p>The concept of a “record” is often mistaken as a single value. For example, “I read for 30 minutes today.” But this value is actually the result of multiple events. It includes when reading started, when it ended, and what happened in between. If you ignore that process and only store the result, the data becomes compressed. Compressed data is simple, but it loses explanatory power. At the time, I failed to recognize this difference. I assumed there were no records, and believed that adding records would solve the problem.</p><p>This is where the direction already begins to diverge. Simplifying a problem makes it seem easier to solve. But if that simplification removes the essence of the problem, every subsequent design will be built on a flawed foundation. Instead of asking how to define records, I focused on adding them. I never reached the stage of questioning the structure itself. Therefore, the statement “there are no records” was fundamentally a wrong problem definition. More precisely, I did not understand how records should be modeled.</p><p>This difference later becomes a dividing line in system design. A system that simply adds records and a system that represents the flow of time are completely different. The former is state-driven, while the latter is event-driven. At this stage, I did not understand that distinction. So the attempt to solve the problem began, but it was already heading in the wrong direction. This section exists to reveal exactly where that misalignment started.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-85.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-85.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-85.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-85.png 1536w" sizes="(min-width: 720px) 720px"></figure><h2 id="why-existing-apps-failed-%E2%80%94-the-%E2%80%9Ccompletion-centric-model%E2%80%9D">Why Existing Apps Failed — The “Completion-Centric Model”</h2><p>After defining the problem as the “absence of records,” the first step was to re-examine existing apps. Since countless reading apps already existed, it seemed reasonable to assume the solution was already there. And indeed, the apps were well-built. Their statistics were detailed, their designs polished, and their user experiences smooth. But as I continued using them, something felt off. Every app seemed to be oriented in the same direction. That direction was not the process, but the outcome.</p><p>Most apps were built around “completion.” Metrics like how many books were finished, how much of a yearly goal was achieved, ratings, and summaries were central. This structure is easy to understand. Outcomes are clear, measurable, and easy to compare. From a system design perspective, this is advantageous. But there is a hidden assumption behind this model. The assumption that the process of reading is not important. The process is treated as a means to an end, and only the result is considered worth recording.</p><p>This assumption shapes the user experience in a specific way. Users are guided toward the goal of “completion.” But this structure fails to answer certain types of questions. For example, how many attempts it took to finish a book, why reading stopped at a certain point, or how much time was actually invested. These are questions about the process, not the result. And process cannot be represented as a single value.</p><p>The key point here is that this is not a feature gap, but a limitation of the model itself. Adding more features does not solve the problem. Because the underlying data structure cannot accommodate those questions. Initially, I did not fully understand this distinction. I simply thought, “the features I want are missing.” But as I analyzed further, it became clear that the issue was structural. Existing apps were not wrong; they were solving a different problem.</p><p>Eventually, I reached a conclusion. What I was looking for was a fundamentally different type of data. Not outcomes, but flow. Not state, but time. Not a single value, but a collection of events. However, even this conclusion was incomplete. Because I still did not know how to model that data. So the next step begins. An attempt to redefine the problem, leading into the next collapse of design.</p><h2 id="what-i-wanted-%E2%80%94-not-%E2%80%9Cresults%E2%80%9D-but-%E2%80%9Cflow%E2%80%9D">What I Wanted — Not “Results,” but “Flow”</h2><p>After examining existing apps, what remained was not a simple dissatisfaction. It was not a matter of missing features, nor was it a problem at the level of poor UX. It was closer to a structural mismatch. What I wanted to see and what the apps were showing existed on entirely different axes. At first, I could not clearly articulate this gap, but as I continued using them, its shape gradually became clearer. What I was curious about was not “how much I read,” but “how I read.” However, existing apps did not deal with that question at all.</p><p>At this point, an important shift occurs. The problem definition changes from “there is no record” to “the direction of the record is different.” I no longer wanted to see results. Instead, I wanted to see the process itself. I needed to understand how many attempts it took to finish a book, why there were days I did not read, and where exactly I stopped in the middle. This kind of flow was not just data; it was a structure moving along a timeline. Because of that, it could not be expressed as a single value in the first place.</p><p>Here, a crucial realization emerges. I did not want to see data; I wanted to see the structure of time. This distinction is larger than it appears. Data can simply be stored, but a time structure must be designed. Time does not organize itself automatically. You must define the unit of segmentation, determine which events to record, and decide how they connect. And those definitions ultimately reshape the entire system. At this point, I began, for the first time, to look at the problem in the right direction.</p><p>However, this was still not a complete answer. The direction was correct, but there was still no method. Wanting to see flow is more of a declaration, and translating it into actual design requires many more decisions. Questions naturally follow: what should be the unit of division, what should be considered a single unit, which data is essential, and which can be discarded. These questions begin to dismantle the previously assumed simple model. At this point, the design develops its first crack.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-86.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-86.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-86.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-86.png 1536w" sizes="(min-width: 720px) 720px"></figure><h2 id="the-first-design-%E2%80%94-a-state-based-model-and-why-it-was-wrong">The First Design — A State-Based Model (And Why It Was Wrong)</h2><p>Even after recognizing the concept of flow, the first design attempt did not escape a simple structure. The most intuitive approach was a state-based model. There is a book as an entity, it has a state, and it has a progress value. This model is intuitive. Most systems are designed this way, so it is easy to accept without much thought. I also adopted this structure initially. Managing state at the book level and updating progress seemed sufficient.</p><p>However, this model quickly reveals its limitations. A state always describes only the “present.” Whether a book is currently being read, completed, or abandoned is meaningful at the current moment, but it does not explain how that state came to be. Progress is the same. A value like 60% does not tell you when that point was reached, whether the process was continuous or fragmented, or what happened along the way. This model is suitable for storing results, but it completely fails at reconstructing the process.</p><p>This problem cannot be solved by simply adding features. Attaching logs or histories on top of a state model does not fundamentally resolve the issue. The reason is that the underlying concept itself is incorrectly defined. A state is a compressed result of a timeline. Therefore, a state alone cannot represent the structure of time. I came to realize this during the design process. The model was not good because it was simple; it was losing something essential because it was simple.</p><p>At this point, a clear conclusion emerges. This model cannot contain the problem I am trying to solve. A structure centered on “current state” fundamentally conflicts with requirements centered on “process.” No matter how much it is extended, it cannot move in the desired direction. This realization signals that the design must be restarted. It is not about refining the structure; it is about changing the underlying concept itself. And at this moment, questions begin to explode.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-87.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-87.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-87.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-87.png 1536w" sizes="(min-width: 720px) 720px"></figure><h2 id="the-moment-questions-explode-%E2%80%94-the-design-begins-to-collapse">The Moment Questions Explode — The Design Begins to Collapse</h2><p>When the state-based model reveals its limitations, the design does not quietly fail. Instead, questions begin to surface all at once. Problems that were previously unconsidered emerge in rapid succession. For example, should the moment reading starts be recorded? Does a one-minute reading session still carry meaning? These questions may appear to be minor details, but in reality, they directly impact the structure of the model. How each question is answered determines the shape of the entire system.</p><p>What matters here is not the number of questions, but their direction. The increase in questions does not mean the problem has become more complex. Rather, it is evidence that the existing model cannot accommodate these questions. For instance, deciding where to store progress is not just about choosing a location. Depending on whether this value is treated as a state, an event, or a snapshot, the entire structure changes. If this question is not answered clearly, data consistency will eventually break, and the UI and logic will start to conflict.</p><p>At this stage, the design can no longer maintain stability. The previously established structure begins to shake piece by piece. A single small question forces a re-evaluation of the entire model. At this point, I realize something critical. The issue is not that I am asking too many questions, but that the original model was never designed to handle them. In other words, the design is failing to keep up with reality. And as this gap widens, maintaining the existing structure becomes increasingly difficult.</p><p>Ultimately, this phase is closer to a <strong>collapse</strong> than a revision. Attempts to solve problems while preserving the existing model become progressively inefficient. As a result, the options narrow down to one. The structure must be split again, and the concept of time must be brought to the center of the model. But this change is not a simple refactoring. It is a shift in how the system itself is perceived. From this point forward, a new design truly begins.</p><h2 id="starting-to-split-the-structure-%E2%80%94-record-session-event">Starting to Split the Structure — Record / Session / Event</h2><p>The moment the model could no longer absorb the questions, the choices narrowed down to two paths. Either keep the existing structure and keep patching it, or redefine the structure itself. The former is easier in the short term. You can add a few fields, handle edge cases in code, and things will seem to work for now. But this approach gradually turns the system into something that is harder and harder to explain. I had already tried going down that path a few times, and I knew the outcome was not good. So this time, I decided to change direction from the beginning.</p><p>The key was to abandon the idea of a single model. Previously, I tried to put everything into the unit of a “book.” State, progress, reading time—everything was expected to fit into a single object. But this approach itself was the problem. I was forcing data with different characteristics into the same layer. So I started separating them. The book-level state remained, but the actual act of reading was split into a separate unit, and the events that occur within that act were further separated.</p><p>This is where the three concepts of Record, Session, and Event emerged. Record maintains the state at the book level. Session represents the unit of actual reading time. And Event records the occurrences that happen within that session. The important point here is not that the model simply increased in number, but that the axis of time was explicitly separated. Previously, time-related information was buried inside the state, but now time itself has an independent structure. This change was not just a structural adjustment, but a shift in how the system is perceived.</p><p>However, even at this point, the design was not complete. In fact, more questions started to arise. Where does a Session begin and end? What criteria define an Event? How should the relationship between Record and Session be maintained? Separation means dividing responsibilities, but it also means defining the boundaries between those responsibilities. So this stage was less of a solution and more of the beginning of new problems. Nevertheless, one thing became clear. There was no going back to the previous state-based model.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-88.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-88.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-88.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-88.png 1536w" sizes="(min-width: 720px) 720px"></figure><h2 id="a-single-field-progresspct-that-revealed-the-essence-of-the-design">A Single Field, progressPct, That Revealed the Essence of the Design</h2><p>As the structure began to split, elements that once seemed trivial started to take on entirely different meanings. A representative example of this was progressPct. At first, I assumed without much thought that this value should belong to the Record. It appeared to be information managed at the book level. Progress seemed like a property of the book’s state, so naturally it was included in the Record. This decision felt intuitive, and I moved on without questioning it.</p><p>But after separating Session and Event, the placement of this value started to feel wrong. Progress is not a fixed value. It changes every time a reading action ends, and in some cases, it can even decrease. If it is stored in the Record, the current state can be represented, but how that value was formed is lost. On the other hand, if it is stored in the Session, each point in time can be recorded, but the overall state is not immediately visible. This was not just a matter of where to store the data, but how to interpret it.</p><p>At this point, a critical shift occurred. Should progress be treated as a “current state,” or as a “snapshot at the end of a session”? Choosing the former leads back to a state-based model, while choosing the latter preserves a time-based model. I chose the latter. progressPct became a value recorded at the end of a session, and the collection of those values forms the overall flow. This decision may seem small, but it locks in the philosophy of the entire system.</p><p>In the end, this single field revealed the essence of the design. This was not about where to store a piece of data, but about how to model time. And with this choice, the system gained a clear direction. Should it be centered around state, or around events? At this point, I became certain that I could no longer return to a state-centered approach. The design was becoming clearer, but at the same time, it was also introducing new constraints.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-89.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-89.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-89.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-89.png 1536w" sizes="(min-width: 720px) 720px"></figure><h2 id="choosing-local-first-%E2%80%94-not-a-solution-but-the-beginning-of-another-problem">Choosing Local-first — Not a Solution, but the Beginning of Another Problem</h2><p>As the structure began to stabilize, the next question was where the system should reside. Most services naturally gravitate toward a server-centric architecture. Login, data synchronization, and backup are assumed as defaults. I initially considered this direction as well. But after thinking it through, it felt like an excessive choice for my case. Questions started to arise. Was it necessary to upload personal data to a server? Could a solo developer maintain a server reliably over time? How would the system behave in an offline environment?</p><p>Answering these questions gradually led me toward a local-first approach. All core data would reside within the device, and the app itself would be a self-contained system. This approach seemed simple. Reducing the server would reduce operational burden, and synchronization issues would appear to disappear. At first glance, it felt like a very reasonable choice. It even seemed like it would lower implementation complexity. But this decision was also built on a set of assumptions.</p><p>A local-first architecture trades simplicity for a different set of constraints. Data consistency must be fully handled on the client side, and state management becomes significantly stricter. Without a server acting as a mediator, all logic must maintain its own consistency. Moreover, when considering future expansion, this structure can become an even greater limitation. At this point, however, I did not fully recognize these implications. I assumed that reducing the server would reduce the problems.</p><p>In the end, this choice was also just another design assumption. Like the earlier state-based model, it too would eventually need to be validated. I thought I had solved the problem, but in reality, I had only shifted it in a different direction. The design was becoming more refined, but at the same time, it was creating new uncertainties. And this trajectory would once again be shaken significantly in the next stage.</p><h2 id="conclusion-of-this-section-%E2%80%94-i-failed-to-properly-define-the-problem">Conclusion of This Section — I Failed to Properly Define the Problem</h2><p>If you follow the flow up to this point, one recurring misconception becomes clear. I believed I was continuously solving the problem. I identified the discomfort that “there is no record,” and I tried to resolve it by creating structures, dividing models, and organizing data. Through that process, I was convinced that I was moving toward a more refined design. However, what actually happened was slightly different. I was not solving the problem; I was repeatedly redefining it. And while each redefinition became slightly more precise than the last, none of them were correct from the beginning.</p><p>The initial starting point was “there is no record.” This statement feels intuitive, but it does not actually describe the real problem. The next step shifted to “I want to see the flow.” This helped set a direction, but it was still not something that could be directly implemented. After that, the state model broke down, time units were separated, and an event-driven structure began to emerge. This process certainly appears like progress. However, what matters is that none of these changes were planned from the start. They happened because the existing design could no longer hold. In other words, the design did not evolve—it was pushed forward by its own limitations.</p><p>At this point, one thing must be made explicit. I did not start by trying to build an app. I did not start by trying to implement features. I started by trying to solve a problem. But I failed to define that problem correctly. As a result, the design kept shifting. I formed one assumption, and when it did not align with reality, I replaced it with another. This cycle may look inefficient, but in reality, it was inevitable. Because until the problem is properly understood, no design can remain stable.</p><p>In the end, the conclusion of this section is simple. I started from a fundamentally incorrect state. And that state did not disappear easily. In fact, it became more visible as the design progressed. But this realization is not a failure—it is a starting point. The moment you recognize that the problem was wrongly defined is the moment a real design can begin. This series continues from that exact point. In the next section, I will go deeper into how the domain design—built on top of this flawed problem definition—collapsed once again, and why that collapse was inevitable.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-90.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-90.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-90.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-90.png 1536w" sizes="(min-width: 720px) 720px"></figure>]]></content:encoded>
                </item>
                <item>
                    <title>Tech Gear — Coming Soon</title>
                    <link>https://en-signal.ceak.dev/tech-gear-coming-soon/</link>
                    <guid isPermaLink="true">https://en-signal.ceak.dev/tech-gear-coming-soon/</guid>
                    <pubDate>Mon, 06 Apr 2026 23:58:43 +0900</pubDate>
                    <dc:creator><![CDATA[ceak]]></dc:creator>
                    <category><![CDATA[🧰 Gear]]></category>
                    <description><![CDATA[This category is in progress.

It will focus on tools and equipment,
based on actual usage rather than reviews.

Content will be added soon.]]></description>
                    <content:encoded><![CDATA[<p>This category is in progress.</p><p>It will focus on tools and equipment,<br>based on actual usage rather than reviews.</p><p>Content will be added soon.</p>]]></content:encoded>
                </item>
                <item>
                    <title>Design — Coming Soon</title>
                    <link>https://en-signal.ceak.dev/design-coming-soon/</link>
                    <guid isPermaLink="true">https://en-signal.ceak.dev/design-coming-soon/</guid>
                    <pubDate>Mon, 06 Apr 2026 23:58:10 +0900</pubDate>
                    <dc:creator><![CDATA[ceak]]></dc:creator>
                    <category><![CDATA[🎨 Design]]></category>
                    <description><![CDATA[This category is currently being prepared.

It will cover UI, structure, and interaction,
not just visual aesthetics.

Updates will follow.]]></description>
                    <content:encoded><![CDATA[<p>This category is currently being prepared.</p><p>It will cover UI, structure, and interaction,<br>not just visual aesthetics.</p><p>Updates will follow.</p>]]></content:encoded>
                </item>
                <item>
                    <title>Why Logs Disappear in Linux — Understanding stdout, stderr, and Redirection</title>
                    <link>https://en-signal.ceak.dev/linux-stdout-stderr-redirection-logs-not-working/</link>
                    <guid isPermaLink="true">https://en-signal.ceak.dev/linux-stdout-stderr-redirection-logs-not-working/</guid>
                    <pubDate>Mon, 06 Apr 2026 22:55:48 +0900</pubDate>
                    <dc:creator><![CDATA[ceak]]></dc:creator>
                    <category><![CDATA[🎯Docs]]></category><category><![CDATA[📚 Linux I/O and Data Flow — From Streams to Pipelines]]></category><category><![CDATA[Linux]]></category><category><![CDATA[Shell]]></category><category><![CDATA[Logging]]></category>
                    <description><![CDATA[When logs don’t show up in Linux, it’s rarely a configuration issue. This article explains how stdout, stderr, and redirection actually work, so you can understand where your logs really go.]]></description>
                    <content:encoded><![CDATA[<h2 id="the-moment-logs-don%E2%80%99t-appear-%E2%80%94-the-problem-is-not-the-system-but-the-mental-model">The Moment Logs Don’t Appear — The Problem Is Not the System, but the Mental Model</h2><p>The issue of logs not being recorded in Linux is, in most cases, not a problem with the environment or the tools, but the result of misunderstanding the output structure. The same command is executed, yet in some cases logs are written to a file, while in others they only appear in the terminal or are only partially recorded. This situation appears to be abnormal behavior, but in reality, it is the system operating exactly as designed. The problem is not the system, but the fact that we are misinterpreting the structure from a file-centric perspective. This document aims to remove that misconception and explain structurally why logs seem to disappear.</p><p>Many developers understand logs as “results written to a file.” From this perspective, if a file does not exist or there is a permission issue, they conclude that logs are not being recorded. However, in Linux, logs are not written to files from the beginning. Programs output data not to files, but to <strong>streams</strong>. Logs are only written to files when this output is connected to a file. Therefore, the issue of logs disappearing is not a file problem, but a problem of <strong>where the output stream is connected</strong>. Without understanding this distinction, the same issue will continue to occur repeatedly.</p><p>At this point, what matters is not “where the output ended up,” but “through which path the output flowed.” In Linux, output does not follow a single path, but is divided into multiple paths, each operating independently. Without understanding this structure, it is impossible to explain why only part of the output is written to a file while the rest seems to disappear. Therefore, to solve log-related problems, it is necessary to first understand how output is divided and how it flows. To understand this flow, we must begin by redefining the most fundamental concept: the structure of output streams.</p><h2 id="common-fixes-%E2%80%94-they-work-but-they-don%E2%80%99t-explain-why">Common Fixes — They Work, but They Don’t Explain Why</h2><p>When encountering issues where logs are not recorded, most people attempt to solve the problem by slightly modifying commands. The most common approach is to use output redirection. For example, the following command is often used.</p><pre><code class="language-bash">command &gt; log.txt
</code></pre><p>This command sends the program’s output to a file, which makes it appear as though the problem has been solved. However, in reality, only part of the output is recorded, while certain messages still remain in the terminal. At this point, many assume that the command is incorrect or that the program is malfunctioning. In reality, this behavior directly reflects the output structure of Linux. The command is functioning correctly, but we are failing to distinguish between different types of output.</p><p>To address this issue, the following command is commonly used.</p><pre><code class="language-bash">command &gt; log.txt 2&gt;&amp;1
</code></pre><p>In most cases, this command successfully writes logs to a file. However, if the same intention is expressed as follows, the result changes.</p><pre><code class="language-bash">command 2&gt;&amp;1 &gt; log.txt
</code></pre><p>These two commands appear identical at first glance, but they produce completely different results. If this difference cannot be explained, then the problem has not been solved, but merely coincidentally avoided. In other words, we are not truly understanding and using the command, but instead memorizing patterns. In this state, the same problem will recur whenever a new situation arises.</p><p>The core issue here is not command syntax, but the underlying structure. Redirection is not simply a feature that writes output to a file, but a <strong>mechanism that reconfigures output paths</strong>. Without understanding this structure, it is impossible to predict how a command will behave. Therefore, the next step is to reconsider the very notion that logs are stored in files, and examine why that assumption itself is a flawed starting point.</p><h2 id="the-core-issue-%E2%80%94-logs-are-not-files-but-streams">The Core Issue — Logs Are Not Files, but Streams</h2><p>The idea that logs are written to files is an interpretation based on the result. In reality, programs do not write data directly to files. Programs output data to <strong>output streams</strong>. These streams are, by default, connected to the terminal, and without additional configuration, the output appears on the screen. In other words, logs do not initially exist in files; they are only recorded in files when the output is explicitly connected to one. Without understanding this distinction, it is impossible to explain why logs appear to disappear.</p><p>In Linux, output is not a single unified channel, but is divided into two independent paths. One path is used for normal output, while the other is used for error messages. These two paths do not affect each other. Therefore, if only one path is connected to a file, the output from the other path will still appear in the terminal. This is the true cause of the phenomenon where “only part of the logs are recorded.” This behavior is not an edge case, but a fundamental aspect of the Linux I/O structure.</p><p>▶ To understand this concept more deeply: <a href="https://en-signal.ceak.dev/linux-stdout-stderr-redirection/" rel="noreferrer">Understanding Linux I/O Streams — stdin, stdout, stderr, and Redirection</a></p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-56.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-56.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-56.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-56.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>Once this structure is understood, the way we approach log-related problems changes fundamentally. Logs do not fail to appear because output is not generated, but because <strong>the output path is not properly connected</strong>. In other words, the problem lies not in the data, but in the path. With this perspective established, all subsequent concepts become naturally connected. The next step is to examine in detail why these output paths are separated, and what role each one plays.</p><h2 id="stdout-and-stderr-%E2%80%94-if-you-don%E2%80%99t-understand-why-output-is-split-logs-will-always-be-wrong">stdout and stderr — If you don’t understand why output is split, logs will always be wrong</h2><p>When logs are partially missing or error messages are not recorded in files, the cause is not a simple configuration issue. The root cause is that output is not a single stream, but two independent paths. Many developers assume that a program has only one output, but in Linux there are fundamentally two output streams. One path delivers normal results, and the other delivers errors or exceptions. These two paths do not interfere with each other and operate as completely separate flows. Without understanding this structure, it is impossible to explain why logs are split or appear to be missing.</p><p>stdout is the path used to deliver the normal execution results of a program. This stream is, by default, connected to the terminal and is intended to display results that users need to see. In contrast, stderr is a separate path used to deliver abnormal conditions such as errors or warnings. This stream operates completely independently from stdout. This separation is not an arbitrary design choice, but a structural decision to control data flow. If normal output and error output are mixed, post-processing and automation become difficult, which is why Linux was designed from the beginning to separate these into independent streams.</p><p>A key characteristic of this structure is that both streams exist simultaneously. When a program runs, stdout and stderr each send data through separate channels. Therefore, if only stdout is redirected to a file, stderr will still be printed to the terminal. This is the direct cause of the phenomenon where “only part of the log is recorded.” In other words, the logs did not disappear; they were output through a different path. This behavior is not an exception, but the fundamental operation of Linux I/O.</p><p>▶ To understand this concept more deeply: <a href="https://en-signal.ceak.dev/linux-stdout-stderr-redirection/" rel="noreferrer">Understanding Linux I/O Streams — stdin, stdout, stderr, and Redirection</a></p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-57.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-57.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-57.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-57.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>This leads to an important question. How are these two output paths controlled. Knowing that they exist is not sufficient. Each path must be directed to a desired destination, and in some cases merged or separated as needed. The mechanism that controls this behavior is redirection.</p><h2 id="redirection-%E2%80%94-it-does-not-change-output-it-changes-the-path">redirection — It does not change output, it changes the path</h2><p>Redirection does not modify the output content. Redirection determines <strong>where the output will flow</strong>. Without understanding this distinction, it is impossible to explain why the same command produces different results. A program always outputs data to stdout and stderr in the same way. Redirection only changes the destination where that output arrives. In other words, the output itself remains unchanged, but the path it takes is altered.</p><p>For example, the <code>&gt;</code> operator changes the destination of stdout to a file. This operator does not intercept or transform the program’s output, but replaces the target that stdout is connected to with a file. As a result, the program continues to write to stdout, but that data is delivered to a file instead of the terminal. Meanwhile, stderr is not affected at all. This is why, when only stdout is redirected to a file, stderr still appears in the terminal. This behavior occurs because redirection applies only to specific streams.</p><pre><code class="language-bash">command &gt; out.log 2&gt; err.log
</code></pre><p>This command sends stdout and stderr to different files. The important point here is that the two streams are controlled independently. Each stream is connected to a different destination through separate redirection operations. Once this structure is understood, it becomes clear that separating or combining logs is not a matter of toggling an option, but a process of reconstructing stream paths.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-58.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-58.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-58.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-58.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>Redirection becomes more powerful when used together with pipes. Pipes are responsible for connecting programs, while redirection controls where output is sent. These two mechanisms serve different purposes, but both operate on the same stream structure. Therefore, to fully understand redirection, it is necessary to understand how streams are identified and connected at a deeper level. This is where the concept of file descriptors emerges. Without understanding this structure, expressions like <code>2&gt;&amp;1</code> will always remain confusing.</p><h2 id="21-%E2%80%94-it-is-not-syntax-it-is-a-connection-between-file-descriptors">2&gt;&amp;1 — It is not syntax, it is a connection between file descriptors</h2><p><code>2&gt;&amp;1</code> is not merely syntax, but an expression that reconstructs the relationship between file descriptors. Without understanding this, it is impossible to explain why logs are merged or separated. Here, <code>1</code> represents stdout, and <code>2</code> represents stderr. These numbers are not arbitrary labels, but file descriptors that identify each stream. In Linux, all input and output operations are handled through file descriptors, which makes these numbers fundamental elements that determine actual data flow.</p><p><code>2&gt;&amp;1</code> means that stderr is connected to the same destination as stdout. The critical point is that this does not copy values, but <strong>links references</strong>. In other words, stderr follows whatever target stdout is pointing to. Because of this structure, the order of commands directly affects the result. For example, the following two commands produce different outcomes.</p><pre><code class="language-bash">command &gt; log.txt 2&gt;&amp;1
</code></pre><p>In this case, stdout is first redirected to a file, and then stderr follows stdout, so both streams are recorded in the file.</p><pre><code class="language-bash">command 2&gt;&amp;1 &gt; log.txt
</code></pre><p>In this case, stderr first follows the original stdout, and then only stdout is redirected to the file, so stderr continues to be printed to the terminal.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-59.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-59.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-59.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-59.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>This difference is not a matter of syntax, but a matter of when and how file descriptors reference their targets. In other words, redirection is not about parsing strings, but about reconstructing file descriptors at execution time. Once this structure is understood, <code>2&gt;&amp;1</code> is no longer something to memorize, but something whose behavior can be predicted.</p><p>At this point, the flow becomes clear. Output is divided into two streams, redirection changes their paths, and file descriptors implement those connections. Understanding these three elements makes it possible to explain why logs appear to disappear. The next step is to analyze how this structure manifests as real problems in actual environments, and how these patterns repeat in practice.</p><h2 id="why-pipe-does-not-pass-stderr-%E2%80%94-misunderstanding-the-connection-structure-always-breaks-logs">Why pipe does not pass stderr — misunderstanding the connection structure always breaks logs</h2><p>If error logs are not passed to the next command when using a pipe, this is not a configuration issue but a misunderstanding of the underlying structure. Many developers assume that the <code>|</code> operator forwards all output to the next process. However, in reality, a pipe connects only one stream. If this structure is not understood, situations repeatedly occur where data is passed but errors disappear. This problem is not caused by the tool itself but by a misunderstanding of how streams are structured.</p><p>A pipe connects only stdout to the stdin of the next process. At this point, stderr is not connected. This behavior is not a limitation but an intentional design decision. A pipe is a mechanism for connecting data flow. stderr exists as a separate channel for delivering errors, so it is not included in the data flow by default. Therefore, in a structure like <code>command1 | command2</code>, stdout from command1 is passed to command2, while stderr is still printed to the terminal. This behavior is not exceptional but the default.</p><p>The core of this structure is the independence of streams. stdout and stderr each have different file descriptors. A pipe operates only on file descriptor 1, which is stdout. Therefore, if stderr must also be passed, the two streams must be explicitly merged into one. This is where <code>2&gt;&amp;1</code> is used. In other words, stderr must first be connected to stdout, and then the pipe must be applied so that both outputs are passed together.</p><pre><code class="language-bash">command1 2&gt;&amp;1 | command2
</code></pre><p>This command connects stderr to stdout and then passes the merged stream to command2. The order is important because the pipe operates only on stdout. If written as <code>command1 | command2 2&gt;&amp;1</code>, only the stderr of command2 is modified, while the stderr of command1 is still printed to the terminal. This difference is determined by which stdout the pipe connects at which moment.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-61.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-61.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-61.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-61.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>This structure explains why logs do not pass through a pipe. The issue cannot be solved by simply adding redirection. The streams must first be merged, and only then connected. At this point, the next step is to examine how this output flow behaves differently in actual execution environments. In particular, this structure behaves very differently outside of a terminal.</p><h2 id="non-terminal-environments-%E2%80%94-the-real-reason-logs-disappear-is-the-execution-context">Non-terminal environments — the real reason logs disappear is the execution context</h2><p>When the same command prints logs normally in a terminal but fails to leave logs in background execution or service environments, the issue is not with redirection or stream configuration but with differences in execution context. A program always writes to stdout and stderr, but the result changes depending on what those streams are connected to. In other words, the problem lies not in the program but in the environment it is attached to.</p><p>A process executed in a terminal has stdout and stderr connected to the terminal device by default. In this case, output appears immediately on the screen. However, when running in the background using <code>nohup</code> or <code>&amp;</code>, the process is detached from the terminal. In this state, stdout and stderr no longer point to the terminal. <code>nohup</code> by default redirects stdout to a file called <code>nohup.out</code>, and stderr follows stdout. Therefore, without explicit configuration, logs are written to an unexpected location.</p><pre><code class="language-bash">nohup command &amp;
</code></pre><p>This command sends stdout to <code>nohup.out</code>, and stderr is also recorded in the same file. However, this behavior can vary depending on the environment. In a systemd-based service environment, stdout and stderr are sent to journald. In Docker, stdout and stderr are collected as container logs. This means that even if the program is the same, the log destination changes completely depending on the execution environment.</p><p>▶ To understand this concept more deeply: <a href="https://en-signal.ceak.dev/nohup-command-linux-background-process/" rel="noreferrer">Linux nohup Fully Explained — How to Keep Processes Running After Terminal Exit</a></p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-62.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-62.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-62.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-62.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>The core of this structure is that the output target is not fixed. stdout and stderr always exist, but their destinations are determined by the execution environment. Therefore, if logs are not visible, it does not mean there is no output, but that it is being sent somewhere else. To resolve this issue, the execution context must be examined instead of the program itself. The next step is to organize how to reliably capture logs in real environments based on this structure.</p><h2 id="controlling-logs-in-practice-%E2%80%94-you-must-design-the-flow-not-the-output">Controlling logs in practice — you must design the flow, not the output</h2><p>Logging is not a matter of adding output commands. Logs are part of a data flow, and the result depends on how that flow is designed. stdout and stderr are independent channels, and their destinations change depending on the execution environment. Without understanding this structure, attempts to manage logs will repeatedly fail. Therefore, controlling logs requires designing the flow of streams, not the output itself.</p><p>In practice, explicitly controlling stdout and stderr is fundamental. The most common approach is to merge both streams into a single file. This is implemented using the form <code>&gt; file 2&gt;&amp;1</code>. This structure sends stdout to a file and connects stderr to stdout so that both are written to the same file. This approach allows logs to be managed in a single file. However, since errors and normal output are not separated, analysis may become more difficult.</p><pre><code class="language-bash">command &gt; app.log 2&gt;&amp;1
</code></pre><p>Alternatively, stdout and stderr can be managed separately. In this case, each stream is redirected to a different file. This structure allows errors to be tracked independently. However, since logs are split across multiple files, management complexity increases. Therefore, the choice depends on system requirements. The important point is that regardless of the approach, the flow of streams must be intentionally designed.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-63.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-63.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-63.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-63.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>At this point, the structure becomes clear. Output is divided into two streams, a pipe connects only one, and the execution environment determines their destination. Understanding these three aspects makes it possible to explain why logs disappear. More importantly, it allows behavior to be predicted consistently even in new environments. The next step is to extend this understanding to a broader perspective of designing the entire logging system.</p><h2 id="logs-are-not-output-they-are-a-contract-%E2%80%94-you-must-define-a-structure-that-is-interpreted-consistently-across-the-system">Logs Are Not Output, They Are a Contract — You Must Define a Structure That Is Interpreted Consistently Across the System</h2><p>The reason logs are not being recorded is not a problem of output configuration. This problem is about how logging is defined across the entire system. In many environments, logs are treated as simple strings printed to the console. However, in a real system, logging is an interface. If this interface is not defined, even if stdout and stderr are fully understood, logs will still be recorded inconsistently. Therefore, solving logging issues requires defining the meaning of output at the system level, not just the output method.</p><p>stdout and stderr are not merely output channels. These two streams are interfaces designed to carry different meanings. stdout is the path for delivering normal data flow. stderr is the path for delivering errors or exceptional conditions. When this distinction is maintained, all subsequent processing operates in a stable manner. When this distinction collapses, logs are no longer interpretable data. For example, if all logs are written to stdout, errors cannot be separated. Conversely, if all logs are written to stderr, even normal data is treated as an error. This problem is not about output mechanics, but about a failure in semantic design.</p><p>This structure operates internally based on file descriptors. A process sends data to stdout through fd 1. A process sends data to stderr through fd 2. The operating system handles these two streams separately. However, the operating system does not interpret the meaning of this data. The operating system only transports data. Therefore, the meaning of stdout and stderr must be defined by the application. At this point, logging becomes not just output, but a contract between systems.</p><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-64.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-64.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-64.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-64.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>The reason this structure is critical in practice is that logs are not just recorded, but reused by other systems. Docker collects stdout and stderr as they are. systemd forwards both streams to journald. Log collection systems perform analysis based on this data. If the meaning of stdout and stderr is not clearly defined, the analysis results become distorted. In other words, logs are not merely recorded data, but input data that downstream systems interpret.</p><p>Ultimately, logging issues are not about output, but about structure. stdout and stderr are not physical output paths, but interfaces with defined meaning. The quality of logs is determined by how these interfaces are defined. Therefore, when designing logging, the first question is not “where should the output go,” but “what meaning should the output carry.” Once this criterion is clearly defined, logs remain consistent even when redirection, pipes, or execution environments change. At this point, logging is no longer output, but expands into a data flow between systems.</p>]]></content:encoded>
                </item>
                <item>
                    <title>How Systems Become Impossible to Fix</title>
                    <link>https://en-signal.ceak.dev/how-systems-become-impossible-to-fix/</link>
                    <guid isPermaLink="true">https://en-signal.ceak.dev/how-systems-become-impossible-to-fix/</guid>
                    <pubDate>Mon, 06 Apr 2026 21:43:19 +0900</pubDate>
                    <dc:creator><![CDATA[ceak]]></dc:creator>
                    <category><![CDATA[⚙ Essays]]></category><category><![CDATA[📃 Short Piece]]></category><category><![CDATA[Software Architecture]]></category><category><![CDATA[Debugging]]></category><category><![CDATA[System Design]]></category>
                    <description><![CDATA[Systems don’t collapse all at once.
Validation stops, criteria drift, interpretation disappears—
until fixing becomes nothing more than moving the problem.]]></description>
                    <content:encoded><![CDATA[<h2 id="even-when-validation-is-not-complete-we-stop">Even when validation is not complete, we stop</h2><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-99.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-99.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-99.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-99.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>Right after deployment, a screenshot of the error rate graph is posted in the team channel. The error spikes that had been occurring consistently just a few hours ago have noticeably decreased, and the person in charge says, “It seems stabilized for now,” based on that graph. The people in the meeting each check their dashboards again, zoom into the same time window, and confirm that the change is real. At that moment, someone says, “Isn’t this good enough?” and no one objects. No one checks which cases have disappeared, whether the disappeared errors were actually resolved, or whether they have simply moved to a different path. It is not that the state is unchecked, but rather that it is judged to be a state that does not need to be checked. That judgment quickly leads to the next task, and the change is recorded as “effective.” In that process, missing input conditions or unreproduced cases are no longer addressed, and the remaining issues are treated as if they do not exist.</p><p>This scene is familiar. Improved metrics are shared immediately, the shared metrics replace judgment, and that judgment functions as a signal to end validation. The choice to check only part of the system instead of the whole is repeated, and that choice appears reasonable every time. There is always not enough time to verify all cases, and the pressure to show results is always present, so validation does not end—it gets cut off. Once cut off, validation is never resumed, and subsequent changes accumulate on top of that state. From this point on, the system no longer has a “confirmed previous state,” and the baseline for comparing what has changed disappears as well. It is not that validation has stopped, but that the stopped state is approved as the standard.</p><p><strong>We are not finishing validation, we are approving the moment at which we stop</strong></p><h2 id="without-a-comparison-baseline-change-cannot-be-interpreted">Without a comparison baseline, change cannot be interpreted</h2><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-100.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-100.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-100.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-100.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>A few days later, a similar issue is reported again. This time, the response time for a specific request has become abnormally long. The person in charge immediately modifies the code and repeatedly calls the same request a few times. A case that previously failed now returns a normal response, and he captures the result and shares it. With the statement “It works now,” the change is deployed, and the issue is treated as resolved. However, that test was not conducted under the same conditions as before. The request parameters are partially different, the execution environment already reflects other changes, and there is no record of repeatedly executing the exact same input. Under what conditions it failed before, and under what conditions it succeeds now—those differences are not verified. Because there is no baseline to verify against, the act of verification itself is omitted. Only the result remains, and there is no baseline to interpret that result.</p><p>This pattern also repeats. A single case of “something that didn’t work now works” is quickly shared, and that case is accepted as evidence of change. Maintaining comparisons under identical conditions becomes increasingly difficult, and the cost required to preserve those comparisons is naturally eliminated. If the environment is not fixed, inputs are not recorded, and repeated executions are not performed, then no change can be connected to the previous state. Changes exist, but there is no way to determine in which direction they are acting. In a state where judgment is impossible, interpretation is also impossible, and when interpretation is impossible, decisions are made in a different way. Instead of comparing results, the method of producing results itself begins to change.</p><p><strong>Change without a comparison baseline is not interpretation, but speculation</strong></p><h2 id="because-we-cannot-interpret-we-change-multiple-things-at-once">Because we cannot interpret, we change multiple things at once</h2><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-101.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-101.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-101.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-101.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>When the same problem appears again, this time the scope of modification becomes broader. The person in charge touches multiple points simultaneously without identifying the root cause. Part of the logic is modified, configuration values are adjusted, and related parameters are changed together. Instead of applying each change separately, everything is bundled and deployed at once. After a few calls, normal behavior is observed, and that combination is shared as an “effective approach.” However, which change actually made the difference is never verified. Some elements in that combination may have been unnecessary, or may have even created new problems, but those distinctions are not identified. Since there is no baseline to verify each change individually, the attempt to separate them disappears altogether. A result is produced, but the path that led to that result is not preserved.</p><p>This choice quickly becomes fixed. Instead of repeating single changes, changing multiple things at once begins to appear more efficient. In a state where comparison is impossible, there is no way to judge which method is better, so the method that produces results more quickly is chosen. Experiments are not designed, and combinations continue to grow. At this point, one critical shift occurs. The moment multiple things are changed simultaneously, it becomes impossible to separate the impact of each change. A result exists, but the cause that produced it cannot be explained in parts. An unexplained result is not preserved as a record, but remains only as a lump in memory. What remains afterward is not “what worked,” but the impression of “what was applied together.”</p><p><strong>Simultaneous changes in a state that cannot be interpreted erase causes and turn results into memory</strong></p><h2 id="the-criteria-do-not-stay-one-%E2%80%94-they-multiply">The Criteria Do Not Stay One — They Multiply</h2><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-102.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-102.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-102.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-102.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>At first, it was just a simple exception. A different rule applied only to a specific case, and that rule was meant to complement the existing logic. But if that exception remains over time, it stops being an exception and becomes another rule. The original rule does not disappear and remains intact, and the newly added rule is not removed either. In this way, rules are not replaced—they accumulate.</p><p>The problem begins at this point. A state is created where multiple different rules can be applied simultaneously to the same input. The result can vary depending on which rule is applied, but the basis for choosing between them is no longer clearly defined. In the code, this appears as branching logic, but the reasoning behind selecting a branch has already been pushed outside the code. Eventually, the decision falls to people, and each person begins applying different criteria.</p><p>In this state, the concept of a “correct rule” can no longer be maintained. The rules exist, but there is no rule that defines when each rule should be applied. It becomes a state where there are rules without a rule above them. From that moment on, the system loses consistency. The same input can no longer guarantee the same output, and even the language needed to explain the differences in results begins to disappear.</p><h2 id="the-moment-criteria-disappear-reversibility-is-lost">The Moment Criteria Disappear, Reversibility Is Lost</h2><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-103.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-103.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-103.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-103.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>The core problem is not the coexistence of multiple rules itself. The real issue is that the moment this state is created, the very criteria for determining which rule is correct disappear. In a structure where different rules coexist, it becomes impossible to decide which rule should be removed. Even the act of removal requires another criterion.</p><p>At this point, any attempt to roll back inevitably fails. Removing one rule may restore correctness for some cases, but it breaks others. Removing a different rule causes another set of problems. Because there is no way to determine which state is more “correct,” removal is always perceived as a risk and ultimately avoided. As a result, every rule survives.</p><p>Consequently, the system transitions from a “modifiable state” to a “state that must be maintained.” No choice improves the system as a whole; every choice makes some part of it worse. In this structure, the concept of recovery no longer holds. The moment recovery is attempted, yet another rule is added. From this point on, change is no longer improvement but accumulation, and the system becomes fixed in an irreversible state.</p><h2 id="when-we-cannot-decide-we-copy-and-paste">When We Cannot Decide, We Copy and Paste</h2><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-104.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-104.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-104.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-104.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>When the problem reappears, the person in charge no longer tries to find the root cause. They cannot determine where to make changes. There is no criterion to identify which condition created the issue or which change influenced it, making it nearly impossible to design a new approach. Instead, they recall what “worked before.” Code combinations that once produced correct behavior, branches that prevented issues, configuration values that temporarily stabilized results—these remain in memory. They take that combination and bring it into the current logic. They do not try to understand the code, nor verify why it worked. They simply paste it as is.</p><p>This choice gradually becomes natural. It feels faster and safer to reuse something that has already worked than to write new code. In a state where nothing can be verified, new attempts have a high probability of failure, and there is no baseline to revert to when they fail. On the other hand, any combination that has succeeded even once becomes its own justification. As a result, the codebase accumulates multiple different combinations designed to prevent the same problem. Each combination may work in a specific context, but its role within the overall flow is never explained. Code created at different points in time becomes entangled within a single logic, and their relationships grow increasingly complex.</p><p>From this point onward, development is no longer design—it becomes a repetition of choices. Decisions are not based on what is correct, but on what has worked at least once. These choices accumulate, and the accumulation of choices forms the structure. But this structure is not intentional—it is a collection of copied outcomes. Once this structure is formed, it cannot be reversed. Because no one can explain what each piece means, neither removal nor reconstruction is attempted.</p><p><strong>When judgment becomes impossible, copying replaces design.</strong></p><h2 id="because-we-cannot-understand-it-we-cannot-delete-it">Because we cannot understand it, we cannot delete it</h2><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-105.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-105.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-105.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-105.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>When you open the code, conditions that clearly do not match the current requirements are visible. Parameters that are no longer used, branches that only work under specific values, and fallback logic whose meaning cannot be explained are all still there. Logically, it makes sense to remove them. But in reality, this is where we stop. Because we cannot know why they were added.</p><p>At the previous stage, the meaning of the code is already no longer interpretable. We cannot explain what problem a condition was meant to prevent, nor can we reproduce the same situation. In this state, deletion becomes an experiment. But that experiment cannot be validated because there is no baseline for comparison. We cannot determine whether deleting it removes the issue or simply causes it to reappear through another path. In the end, deletion becomes an “unverifiable risk.”</p><p>So the choice becomes fixed. We do not delete.</p><p>As this decision repeats, every condition in the code loses its meaning. It is no longer “something that exists because it is needed,” but becomes “something that is maintained because it exists.” Once logic is introduced, it remains regardless of whether it can be explained. Nothing is removed. As a result, the structure is not refined—it accumulates.</p><p>In this state, the structure can no longer be reorganized. Because the existing flow cannot be understood, it is impossible to clean it up. When new requirements arrive, the existing code is not modified. Instead, new conditions are added on top of it. In a structure where deletion is impossible, all change happens through addition.</p><p>At this point, the structure is no longer the result of design. It is closer to a form where unexplained decisions have solidified simply because they were never removed. No one knows why something exists, and yet it remains. That state itself replaces structure.</p><p>From this moment on, the system is no longer a “maintained structure,” but a “fixed structure that exists only because it cannot be deleted.”</p><p><strong>In a system that cannot be understood, deletion is not an option</strong></p><h2 id="because-we-cannot-fix-it-changes-only-move-the-problem">Because we cannot fix it, changes only move the problem</h2><figure class="kg-card kg-image-card"><img src="https://en-signal.ceak.dev/content/images/2026/04/image-106.png" class="kg-image" alt="" loading="lazy" width="1536" height="1024" srcset="https://en-signal.ceak.dev/content/images/size/w600/2026/04/image-106.png 600w, https://en-signal.ceak.dev/content/images/size/w1000/2026/04/image-106.png 1000w, https://en-signal.ceak.dev/content/images/2026/04/image-106.png 1536w" sizes="(min-width: 720px) 720px"></figure><p>When an issue occurs, the engineer does not touch the existing code. Instead of solving the problem, they add new branches around the conditions where the issue appears and make it bypass specific cases. The existing flow remains intact, and only new conditions are layered on top. The change is deployed quickly, and in that specific case, the issue appears to be resolved. But this change is not the result of removing the root cause—it is merely the result of temporarily avoiding a specific path. When the same input comes through a different path, the problem reappears. The engineer opens the code again and adds another condition to block that path. In this process, existing conditions are never removed. Because no one knows what impact their removal might have.</p><p>As this pattern repeats, the code becomes layered with conditions added at different points in time. Each condition may prevent a problem in a specific case, but at the same time, it can conflict with others or create unexpected flows. Fewer and fewer people can explain which branch executes first, which condition takes precedence, or how the entire flow works. From this point on, changes are no longer an act of restructuring. They become fixed as an act of adding yet another path while keeping the existing flow untouched.</p><p>The problem does not end there. At some point, no change functions in the direction of reducing issues. Adding one condition may eliminate a specific case, but it simultaneously creates a new issue somewhere else. Changes do not remove problems. They only relocate them. A problem that used to occur in A moves to B. When B is blocked, it appears in C. Each time, new conditions are added, and existing ones remain untouched.</p><p>At this point, the act of “fixing” loses its original meaning. It is no longer a means of recovery. It is no longer a process of removing the cause and restoring the system to a correct state. Instead, it only works as a way to push problems into different locations within the existing structure. No change stabilizes the system—it merely hides specific cases temporarily.</p><p>And when this state repeats, the act of moving problems itself becomes accepted as normal system behavior.</p><p><strong>In a system that cannot be fixed, change is not recovery—it is relocation</strong></p>]]></content:encoded>
                </item>
                <item>
                    <title>Complete Guide to bash vs sh: Syntax, Execution Model, Compatibility, and shebang Explained with Examples</title>
                    <link>https://en-signal.ceak.dev/bash-vs-sh-difference-shell-script-compatibility/</link>
                    <guid isPermaLink="true">https://en-signal.ceak.dev/bash-vs-sh-difference-shell-script-compatibility/</guid>
                    <pubDate>Mon, 06 Apr 2026 08:22:59 +0900</pubDate>
                    <dc:creator><![CDATA[ceak]]></dc:creator>
                    <category><![CDATA[🎯Docs]]></category><category><![CDATA[📚 Complete Guide to Linux Shell Execution]]></category><category><![CDATA[Bash]]></category><category><![CDATA[Shell Script]]></category><category><![CDATA[Linux]]></category>
                    <description><![CDATA[bash and sh may look similar, but they follow different principles. sh focuses on POSIX compliance and portability, while bash adds extended syntax and convenience. This article explains key differences in conditionals, arrays, shebang usage, and execution environments with examples.]]></description>
                    <content:encoded><![CDATA[<h2 id="1-definition-conclusion">1. Definition / Conclusion</h2><p><code>sh</code> is closer to a <strong>minimal common shell interface</strong>, while <code>bash</code> is a shell that adds <strong>extended features on top of that base</strong>. This difference is not just about naming, but about <strong>which syntax is allowed and where a script can run safely</strong>.</p><h2 id="2-key-summary">2. Key Summary</h2><p><code>sh</code> prioritizes compatibility and portability. <code>bash</code> provides extended syntax such as arrays, <code>[[ ]]</code>, and brace expansion. If a script must run safely across multiple environments, <code>sh</code> is appropriate, while <code>bash</code> is suitable when more features are required and the execution environment can be controlled.</p><h2 id="3-why-it-matters">3. Why It Matters</h2><p>The problem starts from the fact that <code>sh</code> and <code>bash</code> look similar on the surface. Both execute commands, and variables and conditionals appear similar, which makes it easy to treat them as interchangeable. However, once scripts include branching logic, string processing, and loops, the differences become immediately visible.</p><p>The limitation comes from the difference between development and runtime environments. A developer may test in an environment where <code>bash</code> is the default shell, while the actual server may link <code>/bin/sh</code> to a lightweight shell such as <code>dash</code>. In that case, code that works locally can fail only in production.</p><p>The solution is to define the standard first. If the script must run across diverse environments, it should be written using <code>sh</code> conventions. If <code>bash</code> features are required, the script must explicitly declare <code>bash</code> in the shebang. Once this boundary is fixed, syntax choices, testing strategy, and deployment behavior become consistent.</p><h2 id="4-examples">4. Examples</h2><h3 id="example-1-difference-between-and">Example 1. Difference between <code>[[ ]]</code> and <code>[ ]</code></h3><pre><code class="language-sh">#!/bin/bash
name="admin"
if [[ "$name" == "admin" ]]; then
  echo "ok"
fi
</code></pre><p>The result is that <code>ok</code> is printed in <code>bash</code>. This works because <code>[[ ... ]]</code> is an extended conditional expression provided by <code>bash</code>. Since this syntax is not guaranteed in <code>sh</code>, it must be rewritten as follows.</p><pre><code class="language-sh">#!/bin/sh
name="admin"
if [ "$name" = "admin" ]; then
  echo "ok"
fi
</code></pre><p>In practice, the second form is safer when the script must run across multiple servers.</p><h3 id="example-2-array-usage">Example 2. Array usage</h3><pre><code class="language-bash">#!/bin/bash
arr=("apple" "banana" "cherry")
echo "${arr[0]}"
echo "${arr[1]}"
</code></pre><p>The result is that <code>apple</code> and <code>banana</code> are printed. This happens because <code>bash</code> supports arrays. In <code>sh</code>, this syntax is not expected to work, so positional parameters must be used instead.</p><pre><code class="language-sh">#!/bin/sh
set -- apple banana cherry
echo "$1"
echo "$2"
</code></pre><p>The practical implication is that as data structures become more complex, <code>bash</code> becomes more convenient, but it also requires a <code>bash</code> runtime.</p><h3 id="example-3-brace-expansion">Example 3. Brace expansion</h3><pre><code class="language-bash">#!/bin/bash
echo file_{1..3}.log
</code></pre><p>The result is expanded into <code>file_1.log file_2.log file_3.log</code>. This happens because <code>bash</code> performs brace expansion. Since this is not guaranteed in <code>sh</code>, a loop-based approach is safer.</p><pre><code class="language-sh">#!/bin/sh
i=1
while [ "$i" -le 3 ]
do
  echo "file_${i}.log"
  i=$((i + 1))
done
</code></pre><p>In practice, predictable behavior is more important than shorter syntax in portable scripts.</p><h3 id="example-4-shebang-difference">Example 4. Shebang difference</h3><pre><code class="language-sh">#!/bin/sh
echo "hello"
</code></pre><pre><code class="language-bash">#!/bin/bash
echo "hello"
</code></pre><p>The output may be the same, but the interpreter is different. As a result, the allowed syntax range is also different. In practice, the script body and the shebang must always match.</p><h2 id="5-practical-usage">5. Practical Usage</h2><p>In Docker entrypoint scripts, lightweight base images may not include <code>bash</code>. Writing entrypoints using <code>sh</code> reduces image dependency and ensures consistent execution behavior.</p><p>In CI/CD scripts, the default shell of the runner may differ from the developer’s local environment. If <code>bash</code> features are required, <code>#!/bin/bash</code> must be explicitly declared and the pipeline must enforce bash execution to align testing and deployment results.</p><p>In system initialization scripts, execution environments are often highly restricted. Using <code>sh</code> reduces the risk of failure caused by unsupported syntax during early boot stages.</p><p>In development helper scripts, the environment can be controlled by the team. In this case, declaring <code>bash</code> allows the use of arrays and advanced conditionals, making scripts shorter and easier to read.</p><h2 id="6-common-mistakes">6. Common Mistakes</h2><h3 id="mistake-1-using-with-binsh">Mistake 1. Using <code>[[ ]]</code> with <code>#!/bin/sh</code></h3><p>The mistake is declaring <code>sh</code> while using <code>bash</code> syntax.</p><pre><code class="language-sh">#!/bin/sh
if [[ "$x" = "1" ]]; then
  echo ok
fi
</code></pre><p>The result is an error such as <code>[[ not found</code>. The reason is that the declared interpreter is <code>sh</code>, while the syntax requires <code>bash</code>. The fix is to either convert the syntax to POSIX form or change the shebang to <code>bash</code>.</p><h3 id="mistake-2-testing-only-in-local-environment">Mistake 2. Testing only in local environment</h3><p>The mistake is assuming that if it works locally, it will work in production. The result is that it works in a <code>bash</code> environment but fails on servers using <code>sh</code>. The reason is that the interpreter differs between environments. The fix is to test scripts using the same interpreter defined in the shebang.</p><h3 id="mistake-3-using-arrays-in-sh">Mistake 3. Using arrays in <code>sh</code></h3><p>The mistake is writing array syntax in a <code>sh</code> script.</p><pre><code class="language-sh">#!/bin/sh
arr=("a" "b")
echo "${arr[0]}"
</code></pre><p>The result is a syntax error. The reason is that <code>sh</code> does not support arrays. The fix is to use positional parameters or redesign the data handling approach.</p><h3 id="mistake-4-assuming-binsh-is-always-bash">Mistake 4. Assuming <code>/bin/sh</code> is always bash</h3><p>The mistake is assuming <code>/bin/sh</code> behaves like <code>bash</code>. The result is inconsistent behavior across distributions. The reason is that <code>/bin/sh</code> is just a path, and its actual implementation varies. The fix is to treat <code>sh</code> as a minimal common interface and code accordingly.</p><h3 id="mistake-5-overusing-extended-syntax">Mistake 5. Overusing extended syntax</h3><p>The mistake is using features such as <code>{1..10}</code>, <code>source</code>, and advanced substitutions without considering compatibility. The result is scripts that work only in specific environments. The reason is that these are extension features. The fix is to separate scripts by purpose, using <code>sh</code> for portability and <code>bash</code> for convenience.</p><h3 id="mistake-6-using-source-in-sh">Mistake 6. Using <code>source</code> in <code>sh</code></h3><p>The mistake is using <code>source</code> in a <code>sh</code> script.</p><pre><code class="language-sh">#!/bin/sh
source ./env.sh
</code></pre><p>The result may be <code>source: not found</code>. The reason is that <code>source</code> is commonly associated with <code>bash</code>. The fix is to use the POSIX-compatible form.</p><pre><code class="language-sh">. ./env.sh
</code></pre><h2 id="7-related-concepts">7. Related Concepts</h2><p>POSIX is a standard that defines common behavior across Unix-like systems, and <code>sh</code> compatibility is closely tied to it.<br>Shebang (<code>#!</code>) determines which interpreter executes the script.<br><code>dash</code> is a lightweight shell often linked to <code>/bin/sh</code> in Debian-based systems.</p><h2 id="8-deep-dive">8. Deep Dive</h2><p>Structurally, <code>sh</code> is designed to maximize common compatibility. It provides fewer features but ensures consistent behavior across environments. In contrast, <code>bash</code> prioritizes expressiveness and convenience by adding more features.</p><p>From a system perspective, production environments are more constrained than development environments. They typically include fewer packages and simpler runtimes. In this context, the limitations of <code>sh</code> act as a safeguard against environmental differences.</p><p>On the other hand, when execution environments are controlled, such as internal tooling, <code>bash</code> becomes advantageous. Its extended features reduce code length and improve readability. The distinction is not about superiority, but about which layer of problems each shell is designed to solve.</p><h2 id="9-summary">9. Summary</h2><p><code>sh</code> is a minimal common standard, while <code>bash</code> is an extended shell. The difference is not stylistic but based on compatibility scope and execution assumptions. If a script must run in diverse environments, <code>sh</code> is appropriate, while <code>bash</code> is suitable when richer features are needed and the environment is controlled. The final rule is simple. <strong>Before writing a script, decide whether it is a <code>sh</code> script or a <code>bash</code> script.</strong></p>]]></content:encoded>
                </item>
    </channel>
</rss>
