The Problem This Article Addresses — Why the Simplest-Looking Feature Was Actually Dangerous
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.
But there is something easy to overlook at this moment. The structure may be clean, but what actually happens 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.
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?

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.
The Initial Assumption — A Toggle Can Be Handled with a Single if-else
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.
The mental model at that time could be summarized in a single piece of code:
if (activeSession exists)
endSession()
else
startSession()
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.
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.
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.
The Beginning of the Collapse — The Moment Questions Start Attaching
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.
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.
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.

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.
The Core Issue — Toggle Is Not a State Change, but Policy Execution
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 activeSession 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, toggleSession was not a button that changes state, but an entry point through which domain rules are executed.
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.
At this point, toggleSession needs to be redefined. It is no longer a function that simply toggles between start and end. It is a flow that executes appropriate domain procedures based on the current state. 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.

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.
The Explosion of Session Termination — When a Simple Action Becomes a Procedure
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 endedAt field. It is a branching point where multiple states are resolved simultaneously and new states are derived as a result.
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.
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.

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.
Not a Fix, but a Redefinition — Turning Toggle into a Flow
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.
This is where the flow-based structure emerges. Now, toggleSession 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 activeSession 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.
check activeSession
→ start flow OR end flow
→ validate inputs
→ execute domain policies
→ create events
→ update record
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.

Now, toggleSession 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.
The Moment Domain Separation Actually Starts Working
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.
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.
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 preserving consistency. 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.

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.
It Is Not Code but Policy — The Structure Revealed Through Pseudocode
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.
For example, if the termination logic is expressed as pseudocode, it takes a form like this.
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)
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.
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 document that makes domain policy explicit.

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.
It Was Not the Design but the Thinking That Changed
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.
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.
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. Making policy visible, and shaping the flow into something controllable.

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.
The Position of This Article — The First Record of “Execution Collapse”
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.
This article exists precisely at that point. This is not about structure, but the first case that records the moment when execution collapses. 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.
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.

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?”
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.