Published on 29 February 2016 by @mathiasverraes
Below is an attempt at illustrating a design/redesign process I went through at a client, who’s started refactoring the core systems their business depends on. Design is the part of software development that is the most messy, the hardest to fit into rules or well-defined processes. In fact, while writing this post, I tweeted:
None of the solutions offered below should be taken as truth. I may have already changed my mind on some of them by the time you read them.
In 2010, I wrote a small library with some Value Objects to represent money, and in 2011, I published a first version. It was a typical open source scratch-your-own-itch thing, that solved a problem with incorrect rounding and conversion in an e-commerce project I was working on. It seems to have scratched many itches, as it got quite popular.
I’ve been a bad parent to the project lately. It’s in a new GitHub organisation now, with @sagikazarmark as the new maintainer, who’s blowing new life into it.
Over the years, a bunch of forks and variations appeared. I welcomed this, as many of them serve a need that my library didn’t. It freed me from having to support all possible use cases, and avoided feature bloat.
Apart from being incomplete, the library suffers from fundamental design flaws, some of which can’t be fixed without breaking it.
This is what we expect from our model:
To understand this post, I’m assuming you know:
Our first instinct is to have the
Money object from Fowler’s PoEAA, consisting of a float* and a currency, which in turn could be an object as well. Both are immutable.
( * You may not want to use the actual float type in your programming language for dealing with money, as you may run into issues with precision. Do your research.)
Currency is an enum type supporting our 10 currencies (A simple assertion will do if your language doesn’t have enums). We can’t initialise the
Currency object with any other value than our 10 currencies. It only uses the 3-letter ISO symbols, anything else is an error. That satisfies requirement 1.
Money’s constructor can round floats with more than 8 decimals. We can add some operations, like
add(Money other) : Money and
multiply(float operand) : Money. They also round to 8 decimals if needed.
Requirement 3 (showing pretty-printed, rounded values to the users), is clearly presentation logic. We should avoid muddying up our domain model for this. So we add a
MoneyFormatter in the presentation layer, which takes a
Money argument and returns a string, such as €5.00 or 5,00€, depending on local standards. Perhaps we even make a HTML widget or helper of sorts, so that our templates don’t need to worry about it.
Our current design doesn’t satisfy the problem of exposing the rounded values over an API. Of course, we could
money.round() : Money seems more in line with our existing methods.
This is where you should start feeling the itch. Have a close look at the type of this method.
money.round() : Money
Technically, most languages don’t distinguish between 1.99 and 1.99000000, but logically, there is an important nuance here.
b is not just any
Money , it is a fundamentally different type of money. Our design doesn’t make that distinction, and just mixes up money, whether it was rounded or not.
Let’s make the implicit explicit. We can rename
PreciseMoney, and add a new type called
RoundedMoney. The latter always rounds to whatever the currency’s default division is.
round() method now becomes very simple:
The chief benefit is strong guarantees. We can now typehint against
PreciseMoney in most of our domain model, and typehint against
RoundedMoney where we explicitly want or need it.
It’s easy to underestimate how valuable this style of granular types can be.
We’re not building this as a generic library, so we don’t need to supply a “complete” API. We can (and we should) only add the methods that we are actually going to use. In our case,
PreciseMoney has a
RoundedMoney has no
toPrecise(). In other words, we can cast
RoundedMoney, but we can’t cast
PreciseMoney. It’s a one way operation.
There’s an elegance to that constraint. Once you round something, the precision is lost forever. The lack of
RoundedMoney.toPrecise() fits our understanding of our domain.
Perhaps we’ll eventually need some operations on
RoundedMoney, (although I’d rather avoid having them). We can choose some smart return types:
Notice the different return types.
Perhaps you’re writing some client code that wants to do multiplication, but doesn’t care about high precision. Chaining methods would suffice here:
Again, this style is very explicit. The compiler or type checker can protect us when we make mistakes against the types, such as passing
RoundedMoney is required.
Converting between different currencies depends on today’s exchange rates. We’ll need to fetch those from some third party web API. Or perhaps, another process is putting that information in a file or database somewhere. We don’t want to leak that kind of detail into our model, so we can have an Exchange interface, and have one or more implementations of our choice. The method on that interface that we’re interested in, takes a
PreciseMoney and a target
Currency, and does the conversion.
Exchange implementations will also need to deal with concerns such as caching today’s rates, to avoid unnecessary traffic on each call.
If fetching + caching + converting sounds like a lot of responsibility for one service, that’s because it is (and this one is actually still very clean as compared to most service classes I see in actual code). These kinds of service classes are fundamentally procedural code wrapped in an object. Even though leakage is reduced thanks to the Exchange interface, the rest of our code still needs to depend on that interface. This makes testing harder, as all those tests will need to mock
Exchange. And finally, (although I agree it’s subtle here), all implementations of
Exchange will need to duplicate the actual conversion, or somehow share code. A heuristic that helps, is to figure out if we can somehow isolate the side-effectful code (fetching, caching) from the domain logic (conversion).
You guessed it, we need to identify a missing concept. Instead of having the
Exchange do the conversion, we can make it return a
ConversionRate instead. This is a Value Object that represents a source
Currency, a target
Currency, and a factor (a float). Value Objects attract behaviour, and in this case, it’s the
ConversionRate object that becomes the natural place for doing the actual calculation.
convert method makes sure that we never accidentally convert USD to EUR using the rate that was actually meant for GBP to BTC. All it needs to do is throw an exception if the arguments have the wrong currency.
The other cool thing here is that we don’t need to pass around
Exchange. Instead, we pass around the much smaller, simpler,
ConversionRate objects. They are, once again, more composable. Already the possibilities for reuse become obvious: for example, a transaction log can store a copy of the
ConversionRate instance that was used for a conversion, so you get accountability.
Exchange is now something that represents the collection of
ConversionRate objects, and provides access (and filters) on that collection. Sounds familiar? This is really just the Repository pattern! Repositories are not just for Entities or Aggregates, but for all domain objects, including Value Objects.
(We could rename
ConversionRateRepository, which would make the pattern more explicit, but remove a name from our Ubiquitous Language. It’s helpful in some contexts. Personally, I’d be more in favour of keeping the name. We can annotate
Exchange as a @Repository, or have it implement a marker interface called
We split up our procedural original
Exchange service into the two simpler patterns, Repository and Value Object. Notice how we have at no point removed any essential complexity, and yet each element, each object, is very simple in its own right. To understand this code, the only pattern knowledge a junior developer would need, is in a few pages of chapters 5 & 6 of Domain-Driven Design.
We still have some issues. Remember that some currencies have a division of 1/00, 1/1000, or 1/100000000.
RoundedMoney needs to support this, in our case for 10 currencies. The constructor is starting to look pretty ugly:
Every time we need to support a new currency, we need to add code here, and possibly in other places in our system. Not a serious issue, but not ideal. A switch statement is often a smell for missing types. (Yes, we could replace this with a map, but it’s really the same thing. And a map wouldn’t fit if we’d be dealing with more interesting behavioural differences between currencies than rounding.)
What happens if we make
RoundedMoney abstracts or interfaces, and factor the variation out into subtypes for each currency?
Each of the
RoundedBTC etc classes have very local knowledge about how they go about their business, such as the rounding switch example above.
Again, we can put the type system to work here. Remember that, per requirement 5, our reporting needs to be in EUR? We can now typehint for that, making it impossible to pass any other currency into our reporting. Similarly, the different compliance reporting strategies we need for requirement 4 can each be limited to the currencies they support.
Sure, we get a bit of class explosion. But so what? It’s much preferable to long methods.
A benefit of having lots of small classes, is that we can get rid of a lot of code. Perhaps our main Bounded Context deals with the 10
PreciseXYZ types, but our Reporting Bounded Context only supports
RoundedEUR. That means there’s no need to support
RoundedUSD etc, as we are not using it. This also implies that we don’t need
round() methods on any of the
PreciseXYZ classes, apart from EUR. Less code means less boilerplate, less bugs, less tests, and less maintenance.
Not supporting a way back from
PreciseEUR is another example of a minimalist interface. Don’t build behaviours that you don’t need or want to discourage.
Another benefit of these small, ultra-single-purpose classes, is that they very rarely need to change. This is Robert C. Martin’s heuristic for the Single Responsibility Principle:
“A class should have only one reason to change.”
A good design allows you to easily add or remove elements, or change the composition of the elements, but rarely requires you to actually change existing code. This in turn leads to fewer bugs and less work.
You may have noticed that in the current design, I have no
Money interface at the top of the object graph. Aren’t
RoundedMoney both a kind of
Money? Don’t they share a lot of methods?
If we are trying to build a model inspired by the real-world, this would make sense. However, we shouldn’t judge our models by how well they fit our hierarchical categorisations. We judge our models by usefulness. A top-level
Money interface adds no value at all; in fact it takes away value.
This may be a bit counterintuitive.
RoundedMoney, although somewhat related, are fundamentally different types. We’ve designed our model for clarity, for the guarantee that we don’t mix up rounded and precise values. By allowing client code the typehint for the generic Money, we’ve taken away that clarity. There’s now no way of knowing which we’re getting. All responsibility for passing the correct type is now back in the hands of the caller.
Is this overdesigned? Perhaps, depending on your context. When you only want to display some prices, it’s likely overkill. With code that decides about huge amounts of money, a design like this helps me sleep at night.
There’s a hint in the language. We don’t say “this thing is designed”, we say “this thing is designed for that purpose”. Overdesign (and underdesign) simply mean “not designed for purpose”, or “designed for a different purpose than the current one”. The model presented here is designed for the purpose of high precision and confidence in our operations with money.
Often, different aspects of our system have different requirements. This is precisely what Bounded Contexts are for: allow us to reason about our system as a a number of cooperating models, as opposed to one unified model. Perhaps our Product Catalog needs a really simple model for money, because it doesn’t really do anything other than displaying prices. Sales and Reporting on the other hand might benefit from our more intricate design.
It’s important to distinguish between “overdesigned” and “impractical”. The solution to overdesign is to remove elements, make it simpler until it just fits. The solution to impracticality, can be to add more abstractions and shortcuts.
is a handful to write. There’s no reason why you shouldn’t add a static factory method, like
Or just a naked function if your language supports it
Or a unicode function
(Unfortunately, $ is used in many languages to mean all kinds of things, none of which are USD. Be creative!)
There is of course no way to defend your code against bad coders. Unless you’re doing code reviews, someone could change the Value Objects to be mutable, and delete the tests. (Funny story: I was once told that the new developer hired to replace me after I left a job, replaced all private methods by public ones, “because it’s more convenient”.)
I do believe though that good design communicates intent. No matter the level of the developers using your code, if they see two types for rounded and precise money, chances are they are at least going to consider that perhaps we can’t just mix the two in operations without thinking about it.
Value Objects are the ultimate composable objects, so we can keep looking for implicit concepts that we can make explicit. For example, we can naively use our money type to represent prices. But what if a price is not so simple? Maybe
Price is composed of a
PreciseMoney and a
Tax object of sorts. Maybe a
ProductPrice is composed multiple
Price objects for different amounts, for example if you offer a discount when buying in bulk.
Value Objects make it easy to build abstractions that can handle lots of complexity at a very small cognitive cost to the developer using it. Finding the abstractions can be costly and hard, but spending the effort here usually impacts code quality and maintainability so much in the long run, that it’s very often worth it.
The concept of using the type system to reduce a whole classes of errors in code without resorting to excessive tests, can be applied to many domains. Recently, I was late for a talk I was doing at a meetup in London, because a certain website’s “Export to Google Calendar” feature didn’t take timezones into account. Timezone bugs are not only a nuisance to a regular traveler like myself, but can have dire consequences.
Haskell’s Time library has separate types for
ZonedTime, and offers functions to convert between them. This gives guarantees from the compiler that a programmer can not easily mix them up, or implicitly converts between them. Ideally, your code should use
UTCTime everywhere, and only convert to and from
ZonedTime when human users are involved. It should be easy enough to build something like this in object oriented languages.
In many environments, dealing with money is too critical to be regarded as a Generic Subdomain. Different projects have different needs and expectations of how money will be handled. If money matters, you need to build a model that fits your specific problem space like a glove. Some of the tools in our belt are small composable building blocks, making the implicit explicit, and using the type system to our advantage. You might end up with something entirely different.
All good design is redesign.
Follow @mathiasverraes on Twitter.
|Advanced Domain-Driven Design||DDD Europe||workshop||Brussels, Paris||2018|
|Design Heuristics||DDD eXchange||keynote||London||April 2018|
|DDD for Messaging Architectures||ExploreDDD||workshop||Denver||Sep 2018|
|Design Heuristics||Kandddinsky||talk||Berlin||Oct 2018|
|Tactical DDD||Kandddinsky||workshop||Berlin||Oct 2018|
This work by Mathias Verraes is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 License.