The sentence “Reading is a flow” was a result

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.

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.

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.

Why the initial design looked so natural

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.

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.

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.

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.

The first crack — progress is not a state

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.

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

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.

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.

The Fundamental Limitation of the State Model — “Only the Present Exists”

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.

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.

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 being applied to a domain where it could not be used.

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.

The Second Crack — Collision with the Event Model

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.

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.

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.

At this point, I could no longer think in terms of “how to combine these two well.” The problem was not integration, but that two fundamentally different concepts were being placed in the same layer. 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.

The Moment of Collapse — The Model Can No Longer Handle Questions

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.

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.

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.

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.

The Criterion for Redesign — “Separation of Responsibilities”

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 what criteria should guide the redesign. 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 the separation of concepts.

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.

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.

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 clearly defining the boundaries of responsibility for each concept.

Record / Session / Event — Not Structure, but Perspective

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.

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.

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.

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.

Removing Progress — The Moment the Domain Becomes Lighter

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 clarifying responsibility.

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.

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.

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.

Design is Defined by Sentences, Not Code

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.

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.

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.

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 clarity of its definitions. Design must exist before implementation, and that existence must first take form in language, not in code.

Conclusion — Design is Not What is “Correct,” but What Remains

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 failed to endure.

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.

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.

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.