The Moment It Seemed Stable — But the Question Didn’t End

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.

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.

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.

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.

The Initial Way of Seeing Timeline — The Temptation to Store State

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.

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.

TimelineEvent
- bookTitle
- progressPct
- sessionDuration
- computedStats

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.

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.

The Beginning of Misalignment — When Timeline Becomes a ‘Result’ Instead of a ‘Record’

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.

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.

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.

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.

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

A shift in the question — what were we actually trying to preserve?

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.

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.

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

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.

The decision — Timeline records only facts

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.

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.

TimelineEvent
- id
- recordId
- eventType
- payload
- createdAtUtc

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. If it can be calculated, it should not be stored. State should not be stored. Only the cause is recorded.

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.

Making events thin — a design that chooses not to store

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.

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.

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.

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.

How Far Should payload Be Allowed — Between Freedom and Control

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.

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

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.

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.

Immutability — The Condition for Timeline to Be Trustworthy

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 trustworthiness — collapses. That is why this structure explicitly allows only the creation of events, and prohibits modification afterward.

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.

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.

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.

How the UI Is Constructed — Separation of Record and Representation

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.

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.

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.

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.

After the Second Collapse — What Changed Was Not the Structure, but the Way of Thinking

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.

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.

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.

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.

Timeline Is Not UI — A Declaration of the Record Layer

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.

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.

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.

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.

The Next Question — Where Should the Record Live

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.

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.

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.

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.