part 1 of this blog post series introduced the db-view approach and discussed its trade-offs. part 2 showed a TodoMVC example app that uses the db-view approach. This part explains how to do side effects in a db-view application.
Side effects are a necessary evil within most applications. The todo app discussed in part 2 would be pretty useless without side effects, like marking a todo as done or creating a new one. Functional programming tries to push these side effects to the "edges of the system", meaning it tries to maximize the percentage of side-effect-free (pure) functions. Datomic takes this to a whole nother level by applying this principle to create a database system that only needs one stateful place.
The first iteration of the db-view approach used the common way to perform side effects within a web application, it sends a HTTP post to the corresponding API endpoint. This one either performs the side effect (like doing a database transaction) or returns a response with a validation error. The 'either ... or' in the previous sentence already indicates that this API endpoint does two things. Therefore it is not as simple as it could be. I took it apart by moving the validation into the side-effect-free part of the application. The result was the command concept of the db-view approach that is discussed below.
The concept of "commands" is nothing new, it is often used in the context of event sourcing and CQRS.
CQRS stands for Command Query Responsibility Segregation. It's a pattern that I first heard described by Greg Young. At its heart is the notion that you can use a different model to update information than the model you use to read information.
A command that marks a todo as done looks like this:
{:command/type :todo/done!
:db/id 17592186045419}
The server encrypts this command map before returning it as part of the db-view output:
{:todo/list
{:todo/list-items
[{:db/id 17592186045419,
:todo/title "Write a blog post",
:todo/done! "g7RWatF6ldi0Xm6LT/vxhiHIKpw+UzF/OU/iVQDmu0zyTDmRZe7ZdIMPBS2YKVooSivGx6+K9xiHgMa7imqibD1RqFoqFrv+X8TEq0YQUlPJcYjSwiN9DTOUZwrOtPX3Veum8OPUljte/5Eu7924aQrJN2mEI2kfG1DF8eGOOqw="
}]
}
}
The frontend can send this encrypted command to the '/command' API endpoint to perform the side effect. Through the encryption the server can ensure that the frontend cannot manipulate the command map (assuming the encryption is safe). Thereby most validations can be done in advance, when the command map is created. Even if a command like :todo/done!
has no user input, the server can validate, if the current user is allowed to perform this command. You could for example share your todo list with a colleague as read-only version. The server can ensure that this colleague cannot mark your todos as done, simply by not including the encrypted :todo/done!
command in the db-view output. In this way many authorization aspects become very simple to handle. The frontend for example just have to check, if the corresponding command map is in the db-view output to decide, if the corresponding button for the command should be enabled or disabled.
An example for a command that has an user input is :todo/edit!
. It let's the user change the title of a todo. To receive the corresponding encrypted commmand the frontend requests the '/db-view/get' API endpoint with this payload:
{:todo/edit {:db/id 17592186045419
:todo/title "New Title"}}
The response with the db-view output looks like this:
{:todo/edit {:todo/edit "MkVO6khjHd5ZcqojwEpwaFRdjTPNmNiDyFJMHEzVGUUhQ1jq30NYQtIJzMrJkqxNiQJLbgHK7iK+IVsjZPhkU0w0yBqzpy1RwSJVjiNrT16XhGjxAcVWX7S0/vBT659NH9VeBnP6nSfmAnmi/mnbWF4200ZmjuVEQssbMSGMGwpDYfbJwMcl/I0BliT1HnHsKvtbjR9Q6hOzEJwDuTnVvw=="}}
A validation of the :todo/title
could for example enforce a minimum length. Let's say we send this db-view input:
{:todo/edit {:db/id 17592186045419
:todo/title "Ne"}}
Then we would receive this db-view output:
{:todo/edit {:error "Title must be longer than 2 characters!"}}
Instead of the encrypted command the server sends the validation error. A more generic API would rather return an error id with some extra information like the expected minimal length. The frontend would then assemble an appropriate error message. Here the error message is directly included in the response, since a db-view API is exclusively written for a single frontend. The internationalization of the error messages can then also be done on the server-side.
Another common topic in the context of APIs are batch requests, meaning executing multiple API calls in one request. This reduces the latency, since only one instead of multiple requests are used. On the other hand things like returning validation errors get more complicated. For the db-view approach batch requests are unnecessary, since you can always introduce a new command type that combines multiple side effects. In this way it is straight forward to execute all side-effects in a single database transaction. Furthermore it reduces the possibilities for an attacker to mess with the order of the API calls or commands.
A drawback of the command concept is that it offers more possibilities for replay attacks. A simple mechanism that is already included in the example app, can help to prevent replay attacks. The server adds an UUID as :command/uuid
entry to each command map, while the command handler adds this :command/uuid
to the transaction entity. There is a unique constraint on the :command/uuid
Datomic attribute. Meaning a second transaction with the same :command/uuid
will fail. Whereby it is ensured that this command is executed at most once (to avoid replay attacks).
This was the last part to introduce the db-view approach. I hope you find it useful and I would be happy to receive feedback (weber.maximilian at gmail.com).