Juggling Value Objects, REST APIs and HTTP bodies

Domain-driven design (DDD) and REST are well-known concepts and practices used by most of the web developers. Both help to achieve better software by promoting a maintainable codebase and expressive APIs. However, there are some cases these ideas do not play well together.

A Quick DDD Recap

DDD defines artifacts to express models from the business domains. We will focus on three: Entities, Value Objects and Aggregates.

An Entity is an object that is defined by its identity. An identity is an identifier that distinguish each entity uniquely, e.g. the social security number of an individual or the primary key in a relational database.

Suppose the following example. We retrieve the same User (id=1) twice from the database. The eql? method returns true despite the objects are allocated in different portions of the memory. That happens because the comparison is based on the User#id.

user_a = User.find(1)
user_b = User.find(1)

user_a.eql?(user_b) # true

A Value Object is an object that is defined by the values of its attributes. It does not have a conceptual identity. One good example is a Location(lat, lng) object. It does not need a unique identifier because its attributes—latitude and longitude—already define it.

They are also immutable, i.e. we cannot change an attribute. Should we need different attributes, we have to instantiate a new object.

When comparing value objects, we take into account the values of the attributes.

caffe_nero  = Location.new(45, -90)
good_coffee = Location.new(45, -90)

caffe_nero.eql?(coffee_shop) # true

An Aggregate is a collection of objects bound together by a root entity called aggregate root. The objects in this collection may be either entities or value objects, e.g. a todo list with a collection of tasks or a line chart with a collection of points, respectively.

Another property of an Aggregate is that the root acts like a boundary that separates objects in the inside—the collection—from the outside. Also, it is the only object that holds references to the internal objects. Therefore, when the root is deleted, all objects from the aggregate are removed as well.

When in a database, only the root should be retrieved directly. The aggregate objects should be obtained through associations.

todo  = Todo.find(1)
tasks = todo.tasks

Time to REST

REST works fine with entities because all operations related to them are based on their identities. However, things can get unease with value objects given the lack of a unique identifier.

Suppose we are creating an app to keep track of heights and weights of people. A person has a set of body measurements along time. Our REST (JSON) API and design would look like this:

POST    /people
GET     /people/:id
DELETE  /people/:id
POST    /people/:id/measurements
DELETE  /people/:id/measurements

UML class diagram

Person objects are entities and Measurement are value objects. In the database we will have a people table and the measurements will be serialized in a column within the table.

Creating a new measurement is pretty straightforward. Send a POST to the endpoint with the values in the request body. But how do we delete a measurement?

Value Objects and the HTTP spec

As said earlier, value objects are identified by the values of their attributes, so we have to find a way to pass these attributes as parameters in the HTTP request.

Sending simple parameters in a GET or DELETE is very easy. The parameters can be passed in the query string as key/value pairs.

However, passing complex objects is slightly complex, because the HTTP spec does not guarantee the presence the of a request body for those types of requests.

From RFC 2616, HTTP/1.1, Section 4.3

A server SHOULD read and forward a message-body on any request; if the request method does not include defined semantics for an entity-body, then the message-body SHOULD be ignored when handling the request.

From RFC 7231, HTTP/1.1 Semantics and Content, Section 4.3.5

A payload within a DELETE request message has no defined semantics; sending a payload body on a DELETE request might cause some existing implementations to reject the request.

Although the spec does not forbid the request body for DELETE and GET requests, it indicates that it SHOULD be ignored.

What should I do then?

One option is to pass all parameters as simple query string key/value pairs, e.g.

DELETE /people/:id/measurements?examined_at=2014-11-22&height=185&weight=105

Another is to follow what the Restful Objects Specification suggests, that is parameters should be serialized and encoded within the URL. For example, to delete

  "measurement": {
    "examined_at": "2014-11-22",
    "height": 185,
    "weight": 105

We would do

DELETE /people/:id/measurements?%7B%0A%20%20%22measurement%22%3A%20%7B%0A%20%20%20%20%22examined_at%22%3A%20%222014-11-22%22%2C%0A%20%20%20%20%22height%22%3A%20185%2C%0A%20%20%20%20%22weight%22%3A%20105%0A%20%20%7D%0A%7D%0A

One last option would be to send the payload as the request body regardless what the HTTP spec say.

The first two solutions strictly follow the HTTP spec. We still need to make sure the length of the request URI does not exceed the limit accepted by the HTTP server—very unlikely though. On the down side, server and client need an extra effort massaging the data.

Sending complex objects in the request body is much simpler and, IMHO, offers a more cohesive solution. GET, POST, PUT/PATCH and DELETE play by the same rules: they all send JSON right into the wire.

Be aware though, some servers and clients may not preserve the request body (which could lead to long daunting debugging hours). Special attention to proxies, they may also silently strip out the body.

Final Words

In case you decide to take the risk and ignore the HTTP spec for the sake of a simpler solution you are welcome. You can, like me, stand on the shoulders of giants like elasticsearch (request body search, delete by query).

I am not advocating against query strings. It just seems to me that when working with complex objects in a JSON API, using the request body is a better solution.

Ultimately, as always, YMMV. As long as you are aware of the possible architectural issues and inform your API consumers, you should not have a problem.