Decoupling application errors from domain models by Mark Seemann
How to prevent application-specific error cases from infecting your domain models.
Functional error-handling is often done with the Either monad. If all is good, the right case is returned, but if things go wrong, you'll want to return a value that indicates the error. In an application, you'll often need to be able to distinguish between different kinds of errors.
From application errors to HTTP responses #
When an application encounters an error, it should respond appropriately. A GUI-based application should inform the user about the error, a batch job should log it, and a REST API should return the appropriate HTTP status code.
Regular readers of this blog will know that I write many RESTful APIs in F#, using ASP.NET Web API. Since I like to write functional F#, but ASP.NET Web API is an object-oriented framework, I prefer to escape the object-oriented framework as soon as possible. (In general, it makes good architectural sense to write most of your code as framework-independent as possible.)
In my Test-Driven Development with F# Pluralsight course (a free, condensed version is also available), I demonstrate how to handle various error cases in a Controller class:
type ReservationsController (imp) = inherit ApiController () member this.Post (dtr : ReservationDtr) : IHttpActionResult = match imp dtr with | Failure (ValidationError msg) -> this.BadRequest msg :> _ | Failure CapacityExceeded -> this.StatusCode HttpStatusCode.Forbidden :> _ | Success () -> this.Ok () :> _
The injected imp
function is a complete, composed, vertical feature implementation that performs both input validation, business logic, and data access. If input validation fails, it'll return Failure (ValidationError msg)
, and that value is translated to a 400 Bad Request
response. Likewise, if the business logic returns Failure CapacityExceeded
, the response becomes 403 Forbidden
, and a success is returned as 200 OK
.
Both ValidationError
and CapacityExceeded
are cases of an Error
type. This is only a simple example, so these are the only cases defined by that type:
type Error = | ValidationError of string | CapacityExceeded
This seems reasonable, but there's a problem.
Error infection #
In F#, a function can't use a type unless that type is already defined. This is a problem because the Error
type defined above mixes different concerns. If you seek to make illegal states unrepresentable, it follows that validation is not a concern in your domain model. Validation is still important at the boundary of an application, so you can't just ignore it. The ValidationError
case relates to the application boundary, while CapacityExceeded
relates to the domain model.
Still, when implementing your domain model, you may want to return a CapacityExceeded
value from time to time:
// int -> int -> Reservation -> Result<Reservation,Error> let checkCapacity capacity reservedSeats reservation = if capacity < reservation.Quantity + reservedSeats then Failure CapacityExceeded else Success reservation
Notice how the return type of this function is Result<Reservation,Error>
. In order to be able to implement your domain model, you've now pulled in the Error
type, which also defines the ValidationError
case. Your domain model is now polluted by an application boundary concern.
I think many developers would consider this trivial, but in my experience, failure to manage dependencies is the dominant reason for code rot. It makes the code less general, and less reusable, because it's now coupled to something that may not fit into a different context.
Particularly, the situation in the example looks like this:
Boundary and data access modules depend on the domain model, as they should, but everything depends on the Error
type. This is wrong. Modules or libraries should be able to define their own error types.
The Error
type belongs in the Composition Root, but it's impossible to put it there because F# prevents circular dependencies (a treasured language feature).
Fortunately, the fix is straightforward.
Mapped Either values #
A domain model should be self-contained. As Robert C. Martin puts it in APPP:
Abstractions should not depend upon details. Details should depend upon abstractions.Your domain model is an abstraction of the real world (that's why it's called a model), and is the reason you're developing a piece of software in the first place. So start with the domain model:
type BookingError = CapacityExceeded // int -> int -> Reservation -> Result<Reservation,BookingError> let checkCapacity capacity reservedSeats reservation = if capacity < reservation.Quantity + reservedSeats then Failure CapacityExceeded else Success reservation
In this example, there's only a single type of domain error (CapacityExceeded
), but that's mostly because this is an example. Real production code could define a domain error union with several cases. The crux of the matter is that BookingError
isn't infected with irrelevant implementation details like validation error types.
You're still going to need an exhaustive discriminated union to model all possible error cases for your particular application, but that type belongs in the Composition Root. Accordingly, you also need a way to return validation errors in your validation module. Often, a string
is all you need:
// ReservationDtr -> Result<Reservation,string> let validateReservation (dtr : ReservationDtr) = match dtr.Date |> DateTimeOffset.TryParse with | (true, date) -> Success { Reservation.Date = date Name = dtr.Name Email = dtr.Email Quantity = dtr.Quantity } | _ -> Failure "Invalid date."
The validateReservation
function returns a Reservation
value when validation succeeds, and a simple string
with an error message if it fails.
You could, conceivably, return string
values for errors from many different places in your code, so you're going to map them into an appropriate error case that makes sense in your application.
In this particular example, the Controller shown above should still look like this:
type Error = | ValidationError of string | DomainError type ReservationsController (imp) = inherit ApiController () member this.Post (dtr : ReservationDtr) : IHttpActionResult = match imp dtr with | Failure (ValidationError msg) -> this.BadRequest msg :> _ | Failure DomainError -> this.StatusCode HttpStatusCode.Forbidden :> _ | Success () -> this.Ok () :> _
Notice how similar this is to the initial example. The important difference, however, is that Error
is defined in the same module that also implements ReservationsController
. This is part of the composition of the specific application.
In order to make that work, you're going to need to map from one failure type to another. This is trivial to do with an extra function belonging to your Result (or Either) module:
// ('a -> 'b) -> Result<'c,'a> -> Result<'c,'b> let mapFailure f x = match x with | Success succ -> Success succ | Failure fail -> Failure (f fail)
This function takes any Result
value and maps the failure case instead of the success case. It enables you to transform e.g. a BookingError
into a DomainError
:
let imp candidate = either { let! r = validateReservation candidate |> mapFailure ValidationError let i = SqlGateway.getReservedSeats connectionString r.Date let! r = checkCapacity 10 i r |> mapFailure (fun _ -> DomainError) return SqlGateway.saveReservation connectionString r }
This composition is a variation of the composition I've previously published. The only difference is that the error cases are now mapped into the application-specific Error
type.
Conclusion #
Errors can occur in diverse places in your code base: when validating input, when making business decisions, when writing to, or reading from, databases, and so on.
When you use the Either monad for error handling, in a strongly typed language like F#, you'll need to define a discriminated union that models all the error cases you care about in the specific application. You can map module-specific error types into such a comprehensive error type using a function like mapFailure
. In Haskell, it would be the first
function of the Bifunctor
typeclass, so this is a well-known function.
Comments
Mark,
Why is it a problem to use
HttpStatusCode
in the domain model. They appear to be a standard way of categorizing errors.David, thank you for writing. The answer depends on your goals and definition of domain model.
I usually think of domain models in terms of separation of concerns. The purpose of a domain model is to model the business logic, and as Martin Fowler writes in PoEAA about the Domain Model pattern, "you'll want the minimum of coupling from the Domain Model to other layers in the system. You'll notice that a guiding force of many layering patterns is to keep as few dependencies as possible between the domain model and the other parts of the system."
In other words, you're separating the concern of implementing the business rules from the concerns of being able to save data in a database, render it on a screen, send emails, and so on. While also important, these are separate concerns, and I want to be able to vary those independently.
People often hear statements like that as though I want to reserve myself the right to replace my SQL Server database with Neo4J (more on that later, though!). That's actually not my main goal, but I find that if concerns are mixed, all change becomes harder. It becomes more difficult to change how data is saved in a database, and it becomes harder to change business rules.
The Dependency Inversion Principle tries to address such problems by advising that abstractions shouldn't depend on implementation details, but instead, implementation details should depend on abstractions.
This is where the goals come in. I find Robert C. Martin's definition of software architecture helpful. Paraphrased from memory, he defines a software architect's role as enabling change; not predicting change, but making sure that when change has to happen, it's as economical as possible.
As an architect, one of the heuristics I use is that I try to imagine how easily I can replace one component with another. It's not that I really believe that I may have to replace the SQL Server database with Neo4J, but thinking about how hard it would be gives me some insights about how to structure a software solution.
I also imagine what it'd be like to port an application to another environment. Can I port my web site's business rules to a batch job? Can I port my desktop client to a smart phone app? Again, it's not that I necessarily predict that I'll have to do this, but it tells me something about the degrees of freedom offered by the architecture.
If not explicitly addressed, the opposite of freedom tends to happen. In APPP, Robert C. Martin describes a number of design smells, one of them Immobility: "A design is immobile when it contains parts that could be useful in other systems, but the effort and risk involved with separating those parts from the original system are too great. This is an unfortunate, but very common occurrence."
Almost as side-effect, an immobile system is difficult to test. A unit test is a different environment than the intended environment. Well-architected systems are easy to unit test.
HTTP is a communications protocol. Its purpose is to enable exchange of information over networks. While it does that well, it's specifically concerned with that purpose. This includes HTTP status code.
If you use the heuristic of imagining that you'd have to move the heart of your application to a batch job, status codes like
301 Moved Permanently
,404 Not Found
, or405 Method Not Allowed
make little sense.Using HTTP status codes in a domain model couples the model to a particular environment, at least conceptually. It has little to do with the ubiquitous language that Eric Evans discusses in DDD.