Implementing a Repository in Hexagonal architecture with TypeScript, Part I: Contract considerations

J.M.
4 min readMay 16, 2021

This article is the first one in a 4-part series. It is centred around the implementation of a Repository pattern and will focus on contract (interface) considerations, design, implementation and finally testing.

The article is written in the context of Hexagonal architecture with DDD practices, using TypeScript and MongoDB.

What is a Contract

Contract is a conceptual term, that represents a formal agreement, between two software modules. In OOP languages, this is typically implemented as an interface . As such it has a name and can consist of 0..n method signatures and 0..n properties.

Generally, through a Contract, one module defines functionality that it requires from other modules in order to execute its own responsibility.

On the other side, another module (or modules) implement the interface, and by doing so, provide the required functionality.

In short, a Contract defines the “what” (e.g. need to persist a record) and the implementing modules define the “how” (e.g. persisting in MongoDB or persisting in a Text file).

In the diagram above, we can see that in Module A, the Service needs Persistence ability, but it doesn’t care about how that is achieved. That instead is the responsibility of Module B.

Contract

Our repository implementation will be driven by a contract owned and provided by the domain layer.

As we can see, it’s a relatively simple contract, which offers two different methods to search for a particular Order by ID or a set of Orders based on basic search criteria (minimum or maximum total price of an Order).

Furthermore there are methods to add, update and delete an Order.

The most important attribute of this interface is that it does not contain any database technology details. Its design goal is to have no coupling to any particular database technology.

This is especially highlighted in the fact that all methods will accept or return the Order — the currency of exchange here is domain objects, not database-based structures. Furthermore the find method uses the OrdersFilter type — this is a type based on domain layer, instead of a filter/selector object based on a particular DB technology.

Let’s consider an alternative flawed interface to further highlight the advantages of a good design.

The only difference in this implementation is that it uses the FilterQuery type from the 3rd party mongodb package instead of our own OrdersFilter type.

What are the actual implications of this?

  1. The interface is now leaking implementational details (the choice of concrete database should be considered an implementation detail). Our Domain layer as well as Application layer should not be aware any outside dependencies. If we were to switch from MongoDB to another database, this interface and any code using it, will need to be updated.
  2. Any code using the find method of this interface (either in the Application or Domain layer) will now be coupled to the mongodb package. This means that if the FilterQuery type were to change in a future version of the package, we may have quite a lot of code that will require to be updated (and re-tested!) as well.
  3. Furthermore the use of theFilterQuery type has another effect and that is exposing of the database structure of an Order. This is because the filter object must contain actual names of properties inside the MongoDB collection. If we were to restructure the DB schema (for example to improve read performance), this would greatly increase it’s cost and risk of bugs, because there is now a lot more code that would need to be updated.

Finally, let’s compare a high-level dependency structure between the 2 cases.

In this case we can see that any changes coming from the 3rd party mongodb package do not propagate past the MongoDbOrdersRepository . That is ideal for the maintainability of our code base. This design ensures that changes to MongoDbOrdersRepository or any downstream code, will not affect Application or Domain layers.

In this case we can see that the dependency of the OrdersRepository interface on type FilterQuery means that the Interface itself can be affected by change in the mongodb package. What’s worse is that transitively the effect of such change may also propagate into any Application Service that is using the OrdersRepository .

Although the difference in the interface implementation may seem small and not that important in the context of the interface itself, it is design mistakes like these that over time make a code-base rigid and hard to further evolve.

Most application go through an initial phase of rapid growth when features are mostly being added as opposed to modified. This balance however often shifts over time as applications mature. It is past the initial growth phase when the dependency structure / architecture of an application can either support further development or slowly halt it to a grind.

--

--