How I ruined the implementation of the shopping cart, several times.

And how you can avoid it.

David Rodenas PhD
7 min readApr 2, 2022
Photo by Alexas_Fotos on Unsplash

I have been a programmer for a long time. I developed and delivered my first commercial shopping cart more than 30 years ago. And it was not the best code. Even after taking a degree in CS, I was still failing to comprehend how to do a proper implementation.

Here I show how I have ruined the implementation several times; but not only the shopping cart, it can be applied to any other code. I help you to understand how it was ruined, and how to fix it. And more important, how to know which implementation fits to each case.

Two core principles in software design

The first thing that I have to explain to you is that there are two core principles that affect every design: derived values and dependency arrows.

Derived Values: are values that change when other values change. The classic example is the age: it changes each year, it is a derived value from birthdate and current date. Another example is the price of the shopping cart: it is derived from product prices and quantity in the cart.

There are two strategies for implementing derived values: computed and materialized. Computed is having a function that computes its value each time that we ask for it. It can be costly, but you always have the correct value, and it is the easier to maintain. Materialized is having a variable that stores the value. It is fast to read, but you have to keep it up to date with each change, that can ruin some implementations.

Dependency arrows: which object needs to know which other object. Dependency arrows are the key for the good design. Before object-oriented programming, and before functional programming, we had always the same scenario: the arrow of dependency was the same of the execution. Now, we can oppose the dependency to the execution. With this technique, you can call a function, but do not know which function will be executed.

OO and Functional programming decouple execution flow from dependency.

This property allows creating more flexible and easy to maintain designs. By having the dependency arrow pointing in the opposed direction of the execution, it allows extending code without changing it. If you know SOLID, it is the basis for the O and the D. An example of it is the Observer pattern:

The Observer pattern applied to a clock application.

In this example, when the Clock ticks, it updates the update of the View, but Clock does not know that the View exists. If we follow the arrows, we can see that what the Clock sees ends at the Observer. Anyone can implement Observer, so they do not need to know that the View exists.

The shopping cart

The basic shopping cart consists in three basic entities: the Cart, the Product, and the LineItems.

A design of a basic shopping cart.

The Product contains the name and the price of the product. We can change the name, and we can change the price.

The LineItem contains, for a given product, the quantity of a product in the cart. It can increment and decrement the quantity.

The Cart contains a list of LineItems, and has the derived values of size and price. The addProduct and removeProduct adds and removes LineItems. The size and price are derived from its LineItems and Product prices.

Implementation: Computed values

The most simple and straightforward implementation of the shopping cart is making size and price computed values.

The shopping cart implementation with size and price as computed values.

The implementation details are:

  • Adds the size function: it computes the size of the cart by adding all quantities of its LineItems.
  • Adds the price function: it computes the price of the cart by multiplying the quantity of each LineItem by the corresponding Product price, and summing all multiplications results.

Implementation: Materialize values

If we think that it might be costly to compute the price and the size each time, we might decide to create materialized values instead of computed values. Because the price and size depends on Product and LineItem, they have to report changes to the cart. It creates new dependencies that create cycle dependencies.

It materializes size and price, and allows Product and LineItem report changes to Cart.

The implementation details are:

  • Adds size and price attributes to cart. They contain the current value.
  • Adds updatePrice to cart to update the price when a Product price changes. Adds a dependency from Product to Cart, so Product can report the change of price.
  • Adds updateQuantity to cart to update size and price when LineItem quantity changes. Adds a dependency from LineItem to Cart, so LineItem can report the change of quantity.

Implementation: Humiliate Product and LineItem

If we want to avoid creating circular dependencies, but keeping materialized values, we can change semantics. We can decide to use Cart as a facade to update the rest of the classes. In this implementation, dependency arrows does not change, but we misplace the responsibilities for changing the name and quantity. As a consequence, we cannot use LineItem or Product to change their values.

It materializes size and price, but forces changes go through Cart.

The implementation details are:

  • Adds size and price attributes to cart. They contain the current value.
  • Removes the changePrice method from Product, and the increment and decrement from LineItem. Adds updatePrice to Product and updateQuantity to LineItem, so they can keep their values up to date.
  • Cart methods addProduct and removeProduct update the size and price attributes. In addition, if there is already a LineItem for that product, it notifies the change with updateQuantity.
  • Cart method changePrice updates the price. In addition, it also notifies to product to change the price with updatePrice.

Implementation: Event Bus

If we want to keep the size and price materialized, but we do not want to change dependency arrows, or we prefer not to humiliate Product and LineItem, we can apply a dependency inversion principle. One common solution in this case is the EventBus. It looks like the Observer pattern, and it uses the same technique to oppose the dependency. So, we make Product and LineItem to emit events of the changes, and we make Cart subscribe to them.

Product and LineItem emits the change events through the EventBus, and Cart listens for them.

The details of the implementation are the following:

  • Adds size and price attributes to the cart. They contain the current value.
  • Adds the EventBus pattern: A singleton of the EventBus with a relation with the interface EventListener. The EventBus allows adding listeners, and it dispatches event to all EventListeners.
  • Product dispatches an event NameChangedEvent when the name changes, and a PriceChangedEvent when the price changes; through the EventBus.
  • LineItem dispatches an event QuanityChangedEvent, when the quantity changes, through the EventBus.
  • Cart implements EventListener and registers itself to the EventBus. It listens for PriceChangedEvent and QuantityChangedEvent.

Why EventBus and not Observer pattern? Because in this context the Observer pattern requires countless calls to addObserver and removeObserver, each time that a line is added to the Cart, it has to observe the Product and the LineItem. But with EventBus, there is only one subscription, the Cart itself.

The right solution

There is no right solution, it depends on the exact needs of your situation. For example:

  • Models fit in memory: When the models fit in memory, we can rely on in the computed values implementation. If we need to read several times the value of size and cart, we can improve the implementation by relying on memorization.
  • Database: Most SQL Databases allows keeping materialized data without hustle, you can write the query, and they hide all the materialization under the hood. In that case, you can go for either computed or materialized without hurting your designs. But although it is easy to materialize it, it is often more common to ask to compute it on the fly. And it works on non-SQL.
  • Microservices: If you are using microservices you cannot have computed values, the network is too slow. If you are following DDD, and using the Aggregate pattern, then probably you already have an EventBus. So, it looks that you need to use the EventBus to keep the derived values materialized.

The implementations of having the values materialized, but without EventBus, are completely discarded. There are really few cases in which they are sufficient. The good part, is that those implementations help to understand the problem, how dependencies work, and which compromises we have to do.

Summary

If you want to ruin the implementation, you have to make compromises about misplacing responsibilities, or create dependency cycles.

But, in general, there is no such thing as the best solution. You have to choose according your needs and your problems. The key is understanding what you compromise in each one, and try to resolve your current problems.

Thanks for the read. If you liked the article, check my most successful stories on Medium to read more. You can also become a Medium member by visiting this referral link.

--

--

David Rodenas PhD

Passionate software engineer & storyteller. Sharing knowledge to advance our skills. Join me on a journey of discovery in the world of software engineering.