Sponsor: Do you build complex software systems? See how NServiceBus makes it easier to design, build, and manage software systems that use message queues to achieve loose coupling. Get started for free.
Why do you create an abstraction? One reason is to simplify the underlying concept and API. Another reason, probably more common, is that the internal implementation might change. While this can be true, it’s not always as straightforward as you’d think. I will give a couple of examples of things to think about when you’re designing an API.
Check out my YouTube channel, where I post all kinds of content accompanying my posts, including this video showing everything in this post.
I will use a Repository as the example in most of this post since it’s pretty common and (generally) understood.
So we have this repository interface to define various ways we can fetch data and add/update/delete data from a data store. One important aspect to note is this abstraction defines that we are, in some ways, also based on collection-based data sets.
As for implementations, generally, there will be one that is using an ORM such as Entity Framework or the database SDK depending on which type of database you are using, such as a document store.
A common example used for another implementation is a cache. This could be in memory or a distributed cache, but I often hear it used as an example of why having an abstraction such as this is useful.
Based on the example of the typical IRepository above, it could be a terrible idea because it doesn’t support the same expected behavior as our other implementation hitting a database. Why? Because the cache is inherently stale.
If your calling code (consumer) depends on the IRepository, previously provided by the DB direct implementation, and you change that to the Cached Repository, the expected behavior will change to the consumer. For example, if you were trying to read your own right, you won’t because your cache is not consistent.
Expectation matters. If you thought your repository was fully consistent and you swapped the implementation and it’s not, that could have some implications you’d need to address. In this situation, you’d more likely want to be explicit about when you would want cache data either through a different interface (ICachedReadRepository) or through parameters/options of the called method to be explicit that it’s ok to return cached (stale) data.
To continue with the repository example, we are returning and persisting entities based on the current state of entities, either with a relational database or a document store. But how would this abstraction hold up if we wanted to instead be event sourcing as a way to persist state?
As an example of persisting current state of a Product Entity in a relational database.
This same entity using event sourcing would be persisting the events in a stream.
This is a very different way of persisting state. The abstraction you create will be based on the implementation you have in mind. This is typical when you end up creating an abstraction after you’ve created the implementation.
Abstractions you create have a certain model in mind. That won’t fit every model. That’s entirely OK. However, realize that your abstractions are based on your understanding of the implementations you have.
Here’s what an event-sourced repository and implementation might look like. Very different from the previous one, as we are concerned about event streams, not tables or collections.
The fundamental idea of appending events to an event stream in comparison to persisting current state changes our abstraction.
The abstraction you create will fit a specific model.
If you create an abstraction with a single implementation in mind, you’re going to build it around that model and that implementation. If you create an abstraction after the fact based on a single implementation, you’ll end up in the same place.
There isn’t anything wrong with this. However, the idea that abstractions are some magical tool that allows you to swap out concrete implementations seamlessly isn’t true. Where this leads is the question, if you’re creating an abstraction for every implementation, why? You’re likely getting it wrong, as you only have a single implementation your abstraction is based around.
Often it’s not until you’ve built many implementations that your abstraction simplifies the underlying concepts that you’re trying to abstract.
Developer-level members of my YouTube channel or Patreon get access to a private Discord server to chat with other developers about Software Architecture and Design and access to source code for any working demo application I post on my blog or YouTube. Check out my Patreon or YouTube Membership for more info.