Undo is one of those features that sounds easy and rarely is. The obvious approach is to copy everything before a change and copy it back afterwards. But to copy everything, you need to see everything. Memento lets an object hand out a snapshot of itself that nobody else can look inside.
| Category | Behavioral |
| Smell fixed | S18: Cart internals are public so undo can copy them |
| Code | patterns/behavioral/memento |
The Scene
Ava changes her mind a few times:
[018] cart.remove customer=cu-1 sku=SHOE-RUN-42
removed SHOE-RUN-42
[019] cart.undo customer=cu-1
undone
cart cu-1 lines=3 subtotal=A$271.66
[020] cart.undo customer=cu-1
nothing to undo
One level of undo, per customer. A small feature, and it works. Let’s look at what it took to build it.
The Smell
To let something outside the cart save and restore it, every field of the cart had to be public:
// SMELL S18: every internal of the cart is a public field, so a caller can copy the state out and back in
internal class Cart(string customerId)
{
public string CustomerId = customerId;
public List<CartLine> Lines = [];
Then, before every change, StoreConsole copies each field out by hand:
// SMELL S18: the caller copies the cart's public internals out before the change
var lines = CopyLines(cart.Lines);
var promo = cart.PromoCode;
var shipping = cart.ShippingMethod;
var giftWrap = cart.GiftWrap;
var insurance = cart.Insurance;
It stores them in five separate dictionaries, one per field. And to undo, it writes them all straight back:
// SMELL S18: the caller writes the saved internals straight back into the cart
cart.Lines = lines;
cart.PromoCode = savedPromo[cart.CustomerId];
cart.ShippingMethod = savedShipping[cart.CustomerId];
cart.GiftWrap = savedGiftWrap[cart.CustomerId];
cart.Insurance = savedInsurance[cart.CustomerId];
Add a field to the cart, say a delivery note, and you have to remember to update the copy-out code (in two places), add a sixth dictionary and update the restore. Forget any one of them and undo half-works. The console knows the cart’s insides better than the cart does.
The Idea
Memento splits undo into three roles:
- The originator (the cart) knows its own state. It can create a snapshot and restore from one.
- The memento (the snapshot) holds that state, but keeps it private.
- The caretaker (the console) keeps the snapshot safe and hands it back on undo, without ever looking inside.
Think of a coat check. You hand over your coat and get a ticket. The ticket is useless to anyone but the cloakroom: you can’t tell what’s in the pockets by looking at it. When you hand the ticket back, you get exactly your coat.
The Refactor
The cart creates its own snapshot, including defensive copies of its lines, so later changes can’t leak into the saved state:
public CartMemento CreateMemento()
{
return new CartMemento(
CopyLines(Lines),
PromoCode,
ShippingMethod,
GiftWrap,
Insurance);
}
The memento’s fields are all private. The only thing it can do is restore the cart it came from:
internal void Restore(Cart cart)
{
cart.RestoreState(lines, promoCode, shippingMethod, giftWrap, insurance);
}
And the console’s five dictionaries collapse into one:
private readonly Dictionary<string, CartMemento> savedMementos = [];
Saving is cart.CreateMemento(), and undoing is cart.Restore(memento). The console has no idea what’s inside, and doesn’t need to.
classDiagram
StoreConsole --> Cart : requests capture and restore
StoreConsole o--> CartMemento : keeps latest snapshot
Cart ..> CartMemento : creates
CartMemento ..> Cart : restores through RestoreState
An honest footnote
If you open the refactored Cart.cs, you’ll notice the fields are still public. That’s the “one pattern, one seam” rule again. Other code in the baseline (pricing, the widgets, the add-on flags) still reads and writes those fields directly, and fixing that belongs to other smells.
What Memento removed is the undo feature’s dependence on them. Undo no longer reaches inside the cart at all. Once nothing else needs those fields, they can finally become private. That’s often how encapsulation is won back in real codebases: one dependency at a time.
What It Cost
5 files: about 149 lines added and 60 removed. The five dictionaries and the hand-copying code are gone, replaced by one small memento class and two methods on the cart.
When Not to Use It
- The state is huge. A memento copies state. Snapshotting a large object before every keystroke can eat memory fast. Sometimes it’s better to record the change instead, which is what Command does.
- The object is immutable. If your type is an immutable record, the old instance already is the snapshot. Just keep a reference to it.
- Nothing needs restoring. Don’t build undo infrastructure for a feature nobody asked for.
Easily Confused With
Command. Both are used for undo, so they get mixed up all the time. A command records the action, and undoes it by doing the opposite. A memento records the state before the action, and undoes by putting it back. Command is great when actions are easy to reverse. Memento is great when they aren’t, but the state is easy to copy. See Command vs Memento for undo.
Try It Yourself
dotnet build patterns/behavioral/memento/Memento.slnx
dotnet test patterns/behavioral/memento/Memento.slnx
dotnet run --project patterns/behavioral/memento/src/CornerStore/CornerStore.csproj -- scenarios/trading-day.json
Exercise: add a DeliveryNote field to the cart. In the memento solution, you change Cart and CartMemento, both in the cart folder. Now count how many places you’d need to touch in the baseline.
The Memento README has the participant table and a sequence diagram. The full change is in DIFF.md.