Operations
This project and documentation are work in progress
Example code: src/lib/operations
Now we have entities, we need a way to perform operations on them.
Real world systems will usually have many such actions:
getting items from a shopping basket
adding items to a shopping basket
registering a user
cancelling an order
publishing an article
looking up a user's address
In our architecture, these operations are written as functions in the src/lib/operations
folder.
What do operations look like?
Let's take a look at one operation in our sample project - adding a book:
So, the addBook
operation:
destructures the
ctx
to get its helper functionsadds a book to the storage layer (which could end up being a database, filesystem, HTTP call...)
waits for that to complete (using async/await)
dispatches a message to an event bus (which could also have any implementation)
waits for the event dispatch to finish
finally, returns the book
As you can see, all operations in NTA have the following type signature:
Let's go over these.
The context parameter
Operations first take a context, which is how dependencies are passed into the function. Note that as far as reasonably possible, an operation never relies on anything not passed into the function itself (i.e. variables in the closure around it). This is important. It means that all dependencies are passed via the context, which has three benefits:
It's easy to test a function like this.
If context comes from adapters, that means we can swap in and out adapters to create new applications, using the same domain code.
It's easier to keep things like database concerns outside of the operation. In fact we don't even talk about databases in the operation, just the abstract concept of 'storage'.
Benefit no. 1 means that tests are very clean and simple to write. You don't have to do extensive mocking.
Benefit no. 2 means an application written in this architecture to be easily converted from e.g. a REST app speaking to a database to a CLI app speaking to an API.
And benefit no. 3 means that, if you're disciplined, you can separate your concerns.
The input parameter
Operations secondly take an input. Often this will be an object containing all the non-context parameters, and will be specified in the entities. Having an io-ts codec for these types means you can easily validate inputs to your operations.
It's important that operations just take two arguments, because as we'll see, the way contexts are supplied at runtime assumes that all operations have this particular type signature.
Operations should be async
This helps simplify the types and makes it low-effort to retrofit async code to otherwise synchronous functions.
Last updated