-
Notifications
You must be signed in to change notification settings - Fork 51
Boundaries
Boundaries are protocols that provide an interface to an external service. You can think of boundaries as border control for your application. Any data coming from or going to an external service should pass through a boundary. The methods in the protocol represent legitimate channels through which an external service can be accessed.
Boundaries should be loosely coupled to the service they wrap. For example, here is an example of a bad boundary:
(defprotocol SQLDatabase
(get-user [db username])
(get-post [db post-id])
(create-post [db subject body]))
This tightly couples what we want from the external service (user and post data) with the implementation (a SQL database).
In contrast, here is an example of good boundaries:
(defprotocol UserDatabase
(get-user [db username]))
(defprotocol PostDatabase
(get-post [db post-id])
(create-post [db subject body]))
A single record can satisfy many protocols, so it's good practice to have small boundaries divided by purpose.
Any functionality specific to the service should ideally be placed behind the boundary. For example, many databases support transactions, but it's bad practice to expose this through the boundary.
So here is another example of a bad boundary:
(defprotocol AccountsDatabase
(with-tx [db callback])
(credit [db to amount])
(debit [db from amount]))
A good boundary would hide this implementation detail:
(defprotocol AccountsDatabase
(transfer [db from to amount]))
Boundaries not only allow you to control how data enters and exits your application; boundaries are also useful for testing. Tests that use a real external service, for example a database, are often slow and difficult to run concurrently. Slow tests that need to be run in serial mean you can test fewer scenarios in a test run.
Testing against real services is important to ensure your tests are accurate, but fast, concurrent tests are needed to maximise the number of test cases. Boundaries allow you to do both. You can test the implementation of a boundary against a real service, but the rest of your tests can use a mock instead. Shrubbery is an example mocking/stubbing library that can be included as part of your dev dependencies for this purpose.