← Contents · Runnable Specifications by Eric Normand · Work in progress · Comments
203
So far in this book, our data model has encoded state at a single
point in time. We modify the state by modifying the data struc-
ture. However, as domain models get more sophisticated, they
usually require a notion of time. For instance, in accounting, we
need to know not just the current account balance, but the histo-
ry of transactions that led to that balance. In this chapter, we’re
going to explore two ways of encoding history and use them to
implement undo in our application.
Chapter objectives
Learn to model time explicitly in a domain.
Understand when modeling time is useful.
Discover how an explicit model of time makes implement-
ing undo easy.
203
Chapter 6
Time Lens
Important terms
history of states
history of mutations
you’ll learn these
terms in this chapter
204 Chapter 6
Implementing Undo in our GUI
The barista sometimes makes mistakes when taking an order.
Some operations are easy to undo. If you accidentally add hazel-
nut syrup with the addAddIn() operation, you can easily remove
it with the removeAddIn() operation. But some operations are not
so easy. If you accidentally delete a coee from an order, how do
you add it back without going through the time-consuming pro-
cess of re-creating it?
Well, a solution has existed for a long time in GUIs. Its called
undo. Undo is an operation that goes back to the previous state. It
is ubiquitous now in applications. But its not always clear how to
implement it.
Undo is interesting because it requires a notion of time. Undo
is kind of like going back in time. So far, we have been concerned
with the state at the present moment, what weve been calling
the current state. When we change the size of the current coee,
we make a copy of the current coee, we change the size of that,
then we discard the original. The new coee becomes the new
current coee.
This kind of model will get us pretty far. But it makes undo very
dicult. If we want to go back one mutation, we cannot because
weve discarded the previous state. This hints at a solution: re-
membering all of the old states. This is one of the techniques we
will learn in this chapter. We call the technique history of states.
There is another way, however, that is more useful and more
ecient, but slightly harder to implement. In this other tech-
nique, instead of storing the states, we store a piece of data rep-
resenting each mutation that occurs in order. We call it a history
of mutations. Storing the mutations makes it easy to understand
the intent of each action in the history, making audits easy. It also
has other benets like ease of storage and making some combin-
ing operations easy to implement.
We will build both types of histories in this chapter.
205Time Lens
What does a history of states look like?
A history of states is simply a sequence of states, going back as far
as you would like to. Each state is a complete and valid state that
you could switch to without modications.
The history of states keeps a sequence of states for each change.
In order to undo, you simply replace the current state with the
previous state and remove the current state from the history.
Lets see how we can implement it.
{
customerID: “123”,
coffees: [{
size: “mega”,
roast: “burnt”,
addIns: {}
}]
}
{
customerID: “123”,
coffees: []
}
...
{
customerID: “123”,
coffees: [{
size: “galactic”,
roast: “burnt”,
addIns: {}
}]
}
{
customerID: “123”,
coffees: [{
size: “galactic”,
roast: “burnt”,
addIns: {
almond: 1
}
}]
}
change the size to galactic
the starting order is
empty
add a default coffee
we store a complete and
valid state after each
change
add almond syrup
more changes if you
need them
Time
206 Chapter 6
Implementing a history of states
The big advantage of a history of states is that it is easy to imple-
ment. We start with our current state, which in our case will be
an order:
let currentOrder = emptyOrder(“customer123”);
Let’s imagine we have existing GUI operations which modify the
current state. Here are two of them. They are built on top of oper-
ations that treat the orders as immutable values:
function GUIaddCoffee() {
currentOrder = addDefaultCoffee(currentOrder);
}
function GUIsetSize(index, size) {
currentOrder = setCoffeeSize(currentOrder, index, size);
}
We can add a history of states. We start with an array to hold it:
let history = [];
Now, when we add a coee we have to push the current state onto
the history sequence:
function GUIaddCoffee() {
history.push(currentOrder);
currentOrder = addDefaultCoffee(currentOrder);
}
function GUIsetSize(index, size) {
history.push(currentOrder);
currentOrder = setCoffeeSize(currentOrder, index, size);
}
We add history.push(currentOrder) to every operation we
want to maintain history for. Remember, the GUI functions mod-
ify the value of the currentOrder by making a copy of the cur-
rent order, modifying the copy, then saving it. That will maintain
a history of the old states as the current state changes.
207Time Lens
Implementing undo with a history of states
Undo is straightforward with a history of states. Here’s what undo
looks like:
function GUIundo() {
currentOrder = history.pop();
}
Two things are happening on this line of code:
1. The last element of the history array is removed.
2. The current order is replaced with the previous last element.
Every time someone clicks undo, the state will go back one step
in the history. But this implementation does not have redo, which
may be useful. Redo is a little more complicated, but not too hard.
To implement redo, we need to store the changes we can move
forward to in time. We need a separate stack:
let redoStack = [];
Then undo looks like this:
function GUIundo() {
redoStack.push(currentOrder);
currentOrder = history.pop();
}
And here’s redo:
function GUIredo() {
history.push(currentOrder);
currentOrder = redoStack.pop();
}
But we also need to clear the redo sequence when we make a
change to the order. I’ll show one because its always the same:
function GUIaddCoffee() {
history.push(currentOrder);
redoStack.length = 0;
currentOrder = addDefaultCoffee(currentOrder);
}
208 Chapter 6
Q&A: Are copies ineicient?
Q: Isnt it inecient to keep every version of the state around
forever?
A: Thats a really great question. And, yes, it can be inecient,
and thats one of the tradeos. Lets discuss it deeper.
There are really three things that are inecient that we
should pick apart:
1. Ineciency of copy-on-write operations
2. Ineciency of storing copies in memory
3. Ineciency of serializing copies
Copy-on-write is the primary way we implement immutability,
whether by discipline or a language feature. It means that instead
of modifying a value, we make a copy and modify that, leaving
the original unchanged. Copy-on-write is less ecient than mod-
ifying the original directly, but it may be less inecient than you
think at rst.
I’ve discussed this in detail in Grokking Simplicity. I will only
say here that you only have to copy the parts that need to change,
so oen much less is copied than you think. The unchanged parts
can be shared. This idea is called structural sharing. Functional
languages design their data structures to do even more structural
sharing.
In exchange for this ineciency, immutable data gives you
some nice properties. Salient here is the ability to share the data
freely and keep it forever. We could implement undo easily by
saving what used to be the current state to an array and keep it
around as long as we want.
Storing those copies in memory does come at a cost. As the
history gets longer and longer, the memory requirements grow.
It doesnt grow as fast as we may assume because of structural
sharing, but it does still grow. Memory usage goes up and there
is more to sort through by the garbage collector. These can be
important factors that make the program feel slow. But typical-
ly these uses of memory are smaller than other things your pro-
gram does. Prole before you jump to conclusions.
209Time Lens
The nal ineciency is the most damning of this approach. If we
want to write the history to disk, the structural sharing is lost. If
we have 100 orders in an array, if we serialize that to JSON, each
order will be serialized independently, even if they share lots of
structure. So the on-disk requirements grow very quickly. For ex-
ample, if I have an array containing the same object 100 times,
like this:
let obj = {abc: “123”};
let arrayWithSharing = [];
for(let i = 0; i < 100; i++)
arrayWithSharing.push(obj);
let arrayWithoutSharing = [];
arrayWithoutSharing.push({abc: “123”});
There is a lot of sharing going on. But when I serialize it, even
though the object is identical in memory, it will take up 100 times
the bytes on disk as serializing one:
JSON.stringify([obj]).length //=> 15 (= 13 + 2 for [])
JSON.stringify( arr ).length //=> 1401 (= 13 * 100 + 99 for , + 2 for [])
Taking up space on disk is one thing, but what is worse if we want
to read that JSON back in, there is no sharing at all. If you need a
very long history, and you want to keep a record of the history for-
ever, it can get expensive to store. You should do the calculations
yourself to determine if you can aord it.
So, yes, keeping a history of states is not the most ecient op-
tion. But the benets are oen worth the price. And if they are
not, keeping a history of mutations has dierent tradeos.
210 Chapter 6
Q&A: Are undo and redo stacks backwards?
Q: When we added to the undo array, we pushed new values
onto the end. But then when we add to the redo stack, it also
goes on the end, but isn’t that backwards?
A: Wow! You’re very observant. Everything is correct, but we
should go through this more carefully.
We are using arrays to implement both the history of states
for undo and the redo stack. Arrays push a new last element onto
an array. And the pop the last element o. That’s how they work.
They are stacks the push and pop on the end.
This works well for the history of states. The most recently
pushed state is the most recent state. The states are in order, with
the oldest state at index 0 and the newest state at the end.
However, the redo stack is not the past, but in some ways the
future. It contains states that occur aer the current state. But
since were pushing them on the end, the oldest state is at the
end, and the newest state is at index 0. So, yes, the redo stack is
backwards.
Everything that we implemented still works. All that it means
is that if you want to show the redo stacks states in order, you’d
have to display them starting from the end and going backwards.
211Time Lens
Use a history of mutations for long-term records of intent
A history of mutations is much more involved than a history of
states. But civilization has been doing it for much longer. The
most common example of a history of mutations is the account-
ing ledger. The ledger does not contain complete states of the ac-
count (the balance) at each step. Instead, the ledger tracks the
sequence of mutations that occur over time (debits and credits).
Keeping the history of mutations gives us a lot of benes. The
rst and easiest to spot is that its easier to store. Mutations are
usually smaller than the state, and they dont duplicate as much
of their data. But a more benecial property is that the mutations
capture the intention of the change. While you may be able to
reconstruct what happened by looking at a sequence of complete
states, it is much easier to understand the history by looking at
the mutations.
For these reasons, a history of mutations is preferred for long
term recording of a history. If you want the history to persist be-
tween runs of the soware or for posterity, a history of mutations
is much better. It takes up less storage and is easier to understand.
The drawbacks are that the mutations require more code to set
up. However, that code is largely mechanical translations of what
you already have. The mutations can also be used in other ways,
like for synchronizing between dierent devices.
On balance, people nd this to be the right tradeo. You’ll nd
this pattern called by dierent names. The most popular name is
event sourcing.
212 Chapter 6
What does a history of mutations look like?
A history of mutations is a sequence of data values that represent
changes to the current state. By starting with the initial state and
“replaying” the mutations in order, we arrive at the current state.
As you can see, each item in the sequence is a data value that
represents a mutation. The data value must be designed as we
designed the data representation of an order and a coee. We will
see how to do that shortly.
Because we need to do more modeling, the history of muta-
tions is more involved than the history of states. In exchange for
more work, you gain some benets, namely:
more compact data storage
capturing user’s intentions
{
operation: “addCoffee”
}
{
operation: “setCustomerID”,
customerID: “123”
}
...
{
operation: “setSize”,
coffeeIndex: 0,
size: “galactic”
}
{
operation: “incrementAddIn”,
coffeeIndex: 0,
addIn: “almond”
}
change the size to galactic
start with an empty order
set the customer id for
the order
add a default coffee
we store a data
representation of the
mutations
add almond syrup
more mutations if you
need them
Time
213Time Lens
Implementing a history of mutations
For now, lets assume that we have the data model of the muta-
tions we need to store already designed. We will see how to de-
sign them very soon.
The rst thing to note is that we do not need to store the cur-
rent order. Instead, the current order is calculated directly from
the history of mutations.
let mutations = []; // OrderMutation[]
Our original GUI operations looked like this:
function GUIaddCoffee() { //=> void
currentOrder = addDefaultCoffee(currentOrder);
}
function GUIsetSize(index, size) { //=> void
currentOrder = setCoffeeSize(currentOrder, index, size);
}
Those need to change because they operate on the currentOrder
variable, which no longer exists. Their implementation is straight-
forward:
function GUIaddCoffee() { //=> void
mutations.push({operation: “addCoffee”});
}
function GUIsetSize(index, size) { //=> void
mutations.push({operation: “setSize”, index: 0, size: “galactic”});
}
To get the current order, we can run a function on the history of
mutations, which will return the current order. We’ll implement
it on the next page, but here is what calling it would look like:
let currentOrder = replayMutations(); // Order
Lets see what that looks like on the next page.
214 Chapter 6
Replay applies mutations iteratively
When we want to calculate the current order from a sequence of
mutations, we need to start from an initial state, iteratively apply
each mutation in sequence, and return the result. This kind of
system is very easy to implement using reduce():
function replayMutations() { //=> Order
return mutations.reduce(applyMutation, initialOrder());
}
This requires two new functions. The easy one is initialOrder():
function initialOrder() { //=> Order
return emptyOrder();
}
applyMutation() is where all of the hard work happens. It has
to interpret all of the mutation data values into mutation opera-
tions. In that way, applyMutation() is a mutation function it-
self. Here is one way to implement it:
function applyMutation(order, mutation) { //=> Order
switch(mutation.operation) {
case “setCustomerID”:
return setCustomerID(order, mutation.customerID);
case “addCoffee”:
return addDefaultCoffee(order);
case “setSize”:
return setCoffeeSize(order, mutation.index, mutation.size);
case “incrementAddIn”:
return incrementCoffeeAddIn(order, mutation.index, mutation.addIn);
...
}
}
applyMutation() needs to understand all of the possible muta-
tions that can be applied. By running reduce over all of the muta-
tions, the result is the the current order.
215Time Lens
An example of converting operations to data representations
We need to convert each operation into a data representation. Our
operations are written as functions, but functions are opaque.
We represent our operations as data because:
Data can be serialized for long-term use.
Data is readable, both by people and computers.
Lets convert setCustomerID() as as example:
function setCustomerID(order, customerID) //=> Order
We start with some representation of a combination. We will use
a JS object.
let mutation = {};
We give the object a single required eld that will be shared
among all operations. The elds name should indicate that it de-
nes the operation this object represents. We will dispatch on the
value of this eld in applyMutation(). Lets call it operation.
If we’ve named the operation well, we use its name for the value.
let mutation = {
operation: “setCustomerId”
};
Then we convert the arguments to data elds in the object. The
rst argument, order, is implied. We dont need to include it in
this history, since all of the mutations in the array are on the
same order.
But the second argument, custom erID, needs to be stored in
the data. We’ll use its name to name the eld. The eld will store
the value to be passed to the operation:
let mutation = {
operation: “setCustomerId”,
customerID: “123”
};
With this object, we have what we need to call the operation:
setCustomerID(order, mutation.customerID)
216 Chapter 6
How to convert mutations to data
Converting operations to data is a mechanical process:
1. Choose a combination to store the mutation
Data representations of mutations are combinations. We will use
a JS object. Refer to the Data Lens Supplement for more options.
2. Choose a eld name to identify the operation
We need a eld to indicate the intended operation which we will
use to dispatch to the correct function in applyMutation(). The
name of the eld should clearly indicate that it identies an oper-
ation by name. Avoid names that are too generic.
Good Examples (clear meaning)
operation
op (short for operation)
mutation
event
intent
Bad examples (too generic)
type
kind
dispatch
3. Identify the operation using the name of the mutation
Use the name to identify the operation. If the name isnt good
enough, change the name of the operation.
4. Convert the arguments to elds
We will use one eld to capture each argument to the mutation.
However, we typically don’t include the rst argument because it
is the state we are representing with the history itself. We name
the elds aer the operation’s arguments.
Operation as function
function setCoffeeSize(
order,
index,
size
) //=> Order
TypeScript Object type
type SetCoffeeSize = {
operation: “setCoffeeSize”;
index: number;
size: Size;
};
Once we convert all operations into data values, we can write the
apply function, which interprets data values back into operations.
217Time Lens
Use identiers for complex arguments (combining functions)
We just saw the process for creating data values for mutations.
However, what if you want to encode a combining function?
There’s an answer for that. Let’s take a closer look at the relation-
ship between mutation functions and combining functions
Mutation function
function setCoffeeSize(order, index, size) //=> Order
Mutation functions take the main entity (order) as the rst argu-
ment, and any parameters as the rest of the arguments. It returns
the modied main entity (order).
Combining function
function mergeOrders(order1, order2) //=> Order
Combination functions take two (or more) of the main entities
(orders) and return the main entity (order).
We’ve see this already. However, we could say that combining
functions are a special type of mutation function, where the pa-
rameters are by coincidence of the same type as the main entity.
If we do that, order2 becomes a parameter, and we could use
the process for creating data values for mutations. However, the
data value would contain an entire order as one of its elements.
Weve lost much of the concision of the data representation. More
importantly, the intent is no longer captured. Where did this oth-
er order come from?
The solution is to use an identier to name the parameter in-
stead of using the value itself:
type MergeOrders = {
operation: “mergeOrders”;
otherOrder: {
id: OrderId;
version: VersionId;
};
};
We include the version number to pinpoint the orders version.
We can do this for any operation with an entity as an argument.
218 Chapter 6
Mutations are an alternative
Youll remember from the Data Lens that whenever we must
choose a single option among many options, we are looking at
an alternative. In this case, we are choosing a single mutation
among many mutations.
We have choices for how to represent mutations. For our pur-
poses, a TypeScript union type will do nicely:
type OrderMutation = SetCoffeeSize |
SetCustomerID |
AddCoffee |
IncrementCoffeeAddIn |
...;
There are, as usual, other choices you could make. For instance,
you could use an interface and classes that implement the inter-
face:
interface OrderMutation {
operation: string;
}
class SetCoffeeSize implements OrderMutation {
operation = “setCoffeeSize”;
index: number;
size: Size;
}
class SetCustomerID implements OrderMutation {
operation = “setCustomerID”;
customerID: string;
}
These may have dierent implementation consequences in your
language. In addition, we’ll learn another perspective that will
help distinguish these two in the Volatility Lens chapter.
219Time Lens
Implement applyMutation() as a big dispatch
We’ve seen an example implementation of applyMutation(). It
is essentially an interpreter that dispatches the mutation to the
correct operation. Any conditional can do the work. JavaScript
includes if/else if/else and switch statements. Ive chosen a switch
statement to make it clear the dispatch works only on a single
value:
function applyMutation(order, mutation) { //=> Order
switch(mutation.operation) {
Then we add a case per operation. In each case, we call the func-
tion that corresponds to the correct operation. We pass in the
order as the rst argument and the other arguments from the
mutation data. Here is the rst one:
function applyMutation(order, mutation) { //=> Order
switch(mutation.operation) {
case “setCustomerID”:
return setCustomerID(order, mutation.customerID);
Be sure to return the order returned from each of the functions.
Aer weve implemented all of them, we can add a default case,
which will trigger in case the operation name is not recognized.
In that case, we fail loudly:
function applyMutation(order, mutation) { //=> Order
switch(mutation.operation) {
case “setCustomerID”:
return setCustomerID(order, mutation.customerID);
...
default:
throw new Error(`Unknown order operation: ${mutation.operation}`);
}
}
And thats it! Remember: We used a switch statement here, but
any form of dispatch on the operation name would work.
220 Chapter 6
Add auditing information to the mutation data
The model of mutations works well for in-memory processing, as
we might need for implementing undo or displaying a history of
versions. However, for long-term use, we will want more infor-
mation. Some of the information we may want to keep around:
The time the operation happened
The identier of the device that initiated the operation
The identier of the user that initiated the operation
A sequential value that could be used to identify the version
These pieces of data could come in handy if we ever need to do an
audit. An audit simply means a methodical review of the history
of an entity, usually to understand either:
If its in the right state
How it got to the state its in
Further, the sequential version identier is useful for other things.
First of all, it lets you sort the mutations in case they ever get
out of order. Second, it allows you to refer to any specic version,
which is helpful for when dealing with complex trees of opera-
tions. For instance, order 3 could be produced by merging order
1 and order 2. But then order 2 continues to be modied. It would
be good to remember that order 3 was produced from order 2 at
v3, not at v4.
Final considerations
Auditing information becomes more useful the longer lived the
data is. If youre just going to use the mutations in memory then
throw them away when the soware exits, it really doesn’t make
sense to record all of these things.
However, if you are serializing the mutations to a database or
le, you will probably want to remember the user and the time.
The device identier is useful for legally important situations,
like banking, medical records, or legal documents. And the ver-
sion identier can help when entities change independently yet
can inuence each other.
221Time Lens
Implementing undo using a history of mutations
Undo in a history of mutations is much the same as in a history of
states: You pop o the last element from the history array. What’s
dierent is that you don’t need to keep the current state. Instead,
you can recalculate it each time.
let mutations = []; // OrderMutation[]
function undo() { //=> void
mutations.pop();
}
Now you recalculate the current state and re-render the GUI.
Redo is also similar. It requires keeping a redo array. The redo
array will get the value popped when you do an undo.
let redoStack = []; // OrderMutation[]
function undo() { //=> void
redoStack.push(mutations.pop());
}
function redo() { //=> void
mutations.push(redoStack.pop());
}
And similar to the history of states, we need to clear the redo
stack whenever we perform an operation:
function GUIaddCoffee() { //=> void
mutations.push({operation: “addCoffee”});
redoStack.length = 0;
}
function GUIsetSize(index, size) { //=> void
mutations.push({
operation: “setCoffeeSize”,
index: 0,
size: “galactic”
});
redoStack.length = 0;
}
222 Chapter 6
Q&A: Is replaying expensive?
Q: Wow! Replaying the entire history each time? I thought
history of states was expensive!
Okay, thats a valid concern. Lets address it.
Yes, replaying the history is less ecient than keeping the
current state around in memory. This may or may not be a real
problem. The current state is calculated once per mutation, and
it is oen faster than repainting the UI in a browser. However,
sometimes it is not faster. We should consider how to speed it up.
First of all, if the history is short enough, in practice it’s not
a problem. For instance, replaying ten mutations could be very
quick.
For longer histories of 100 or 1,000 mutations, a common
practice is to keep a cache of the current state every n mutations
alongside the history. n could be as big or small as you want. I
typically choose n to be between 10 and 100, depending on how
much speed I want. With that cache, you only have to replay at
most the last nine mutations. Just pick the most recent cached
state and replay forward from there. Just know that these caches
are gaining speed at the expense of memory usage.
Another technique is to close the books. This is borrowed
from accounting, where at the end of the year, the physical book
is balanced and a new book is started for the new year. The bal-
ance from the end of the previous book is carried over to the new
book. The old book is “closed”, essentially becoming read-only.
If there is a natural boundary (like the nancial year) in your
domain on which to close the books, this technique could help
you not have to keep a long history in memory.
The important thing is to consider the tradeos. Are recording
the history, maintaining the intentions, and allowing undo valu-
able? Are they valuable enough to incur the cost?
223Time Lens
Histories of states and mutations compared
Lets review the two types of histories and compare them to each
other.
In-memory storage
History of states
Store every complete state
Lots of structural sharing
Easy to switch to any state in the history
History of mutations
Store every mutation
Small amount of data per mutation
Replay mutations from last cached value
Long-term storage (les or database)
History of states
Large, nested structures
No structural sharing
Lots of repetition
Easy to read any state in history
History of mutations
Small amount of data per mutation
Easy to serialize
Replay mutations to get current state
Implementation and undo/redo
History of states
No special implementation required
Uses existing data structures
Undo is fastest
History of mutations
Requires new data structures
Requires applyMutation() interpreter
Undo needs to replay mutations
Value of information
History of states
Useful for undo/redo
Dicult to mine for user intention
History of mutations
Useful for undo/redo
Easy to mine for user intention
Rich source of marketing data
Conclusion
History of mutations is the more mature implementation. While
history of states is easy to implement and can get you some ben-
ets, history of mutations is where the real value is. Thats the
model that accounting has been operating under for hundreds of
years. You should consider using it, too.
224 Chapter 6
Histories in pre-digital information systems
Sometimes when I explain the two types of histories, some peo-
ple complain that they are too complicated. Further, they com-
plain that their model of the real world is now conated with new
operations like applyMutation() and replay().
I am sympathetic to this view. However, many of the world’s
most successful information systems have some notion of histo-
ry. The two I would like to highlight are accounting and medical
records.
Accounting
Accounting is the classical information system. At one time banks
housed gold in vaults. But through the years, they realized that it
was much easier and more valuable to keep track of money--that
is, keep a history of transactions--than to provide a safe place for
the money.
One could argue the same thing for accounting: My model is
simple. I just need to know who has how much money right now,
just like I might only need to know what the curent coee order
is. But thats ignoring the tremendous value of the information.
For instance, I might want to know how much money I had on De-
cember 31 of last year, to calculate my taxes. Keeping the history
helps me do that.
Medical records
Medical records also keep a history. Before electronic records,
doctors had large ling cabinets of folders and papers, document-
ing the medical history of their patients. If I go to the doctor with
the u, they write down my temperature and a diagnosis, along
with the date, and put it in my le. When I go back two weeks lat-
er, they don’t throw away the paper because I got better. No, they
make a new paper, saying the symptoms are gone.
Conclusion
We oen forget that soware for managing a business is an in-
formation system. Soware collects, processes, and distributes
information. Very rarely are we trying to simulate the world.
225Time Lens
Conclusion
The time lens gives us tools for evaluating and developing a no-
tion of time in our models. Mature information system models,
such as accounting, typically include a notion of time. The two
types of histories are the most common ways of representing
time in information systems.
Summary
History of states stores a sequence of complete states. It is
the easiest to implement, since it requies no new data val-
ues, yet it is powerful enough to implement undo and redo.
History of mutations stores a sequence of data values encod-
ing mutations to the state over time. It can be used to imple-
ment undo and redo, but it also enables rich data collection
for audits and understanding.
We can cache states periodically to make replaying muta-
tions faster. This makes history of mutations more practi-
cal.
As models mature, they tend to include a notion of time
which includes a history of mutations along with a time-
stamp, who initiated the mutation, and other useful infor-
mation.
History of mutations is the more sophisticated model of
time, though it requires extra data values and operations.
However, the underlying model does not change, and those
who store the history nd the information it captures valu-
able.
Up next . . .
We learned two ways of explicitly modeling time--which are use-
ful even if we didn’t originally consider time as part of the model.
In the next chapter, were going to learn to reexamine the prob-
lem we are actually solving in order to model the domain more
simply and directly.