This post is part of The Software Architecture Chronicles, a series of posts about Software Architecture. In them, I write about what I’ve learned on Software Architecture, how I think of it, and how I use that knowledge. The contents of this post might make more sense if you read the previous posts in this series.
In my previous post in this series, I published an infographic that reflects the mental map I use to figure out the relationships between the code units types.
However, there was something that I always felt it was not very well reflected there, but I just didn’t know how to do it any better: the shared kernel.
Furthermore, I figured out a few more things, and I’m gonna write about it all in this post!
Looking at the infographic I published in my last post of this series, we see the shared kernel in the very centre of the diagram, looking like it is within the Domain layer and above the conic sections that represent bounded contexts.
Despite its location, I did not mean to imply that the shared kernel depends on the remainder of the code nor that the shared kernel is another layer within the Domain layer.
What is the shared kernel?!
The shared kernel as defined by Eric Evans, the father of DDD, is the code that the development team decides to share between several bounded contexts:
[…] some subset of the domain model that the two teams agree to share. Of course this includes, along with this subset of the model, the subset of code or of the database design associated with that part of the model. This explicitly shared stuff has special status, and shouldn’t be changed without consultation with the other team.
Shared Kernel, DDD wiki by Ward Cunningham
So basically, it can be any type of code: domain layer code, application layer code, libraries, … whatever.
In the context of the mental map I use, however, I think of it as a subset of that, as specific types of code. In my mental map, the shared kernel contains the domain and application layers code that is shared among bounded contexts in order for communication between bounded contexts to be possible.
This means, for example, the events that are triggered in one or more bounded contexts and listened to within other bounded contexts. Together with those events we need to share all data types that are used by those events, for example: entities IDs, value objects, enums, etc. Complex objects like entities should not be used by events directly because they can be problematic to serialize/unserialize into/from a queue, so the shared code shouldn’t propagate much.
Of course that if we have in our hands a polyglot system, composed of micro-services developed in different languages, this shared kernel needs to be descriptive, in json, xml, yaml, or whatever, so that all micro-services can understand it.
As a result, this shared kernel is completely decoupled from the remainder of the codebase, from the components. This is great, as it means that the components, although coupled to the shared kernel, are decoupled between each other. The shared code is explicitly identified and easily extractable to a separate library.
This is also quite handy if and when we decide to extract one of the bounded contexts into a micro-service separated from the monolith, we know exactly what is shared and we can simply extract the shared kernel into a library to be installed in both the monolith and the micro-service.
So to recap, in my mental map, the application core depends on the shared kernel which contains code, from the domain and application layers, that is shared between bounded contexts.
When the language is insufficient…
So, we have the application code with all its concentric layers, and the application core depends on the shared kernel, which sits below all that code.
We can also say that all that code depends on the programming language(s) being used, but that is such an obvious fact that we tend to completely ignore it.
I am bringing it up, however, because of the question “what do we do when the language constructs are not enough?!”. Well, obviously we create those language constructs ourselves and thus compensate for the language flaws. But the important follow up questions I have are “How do we transmit the rationale behind that code existence? Where do we place it? How do we make explicit when and how it should be used?”.
What I’ve seen, and done myself, is a package called Utils or Commons where that code is placed. But this ends up being a bucket where we dump all the code that we don’t know where to put! All kind of different code, with different purposes and usability (wrapped in an adapter, used directly, …) ends up being dumped there, the package has no conceptual meaning, no consistency, no cohesion, no explicitness, plenty of ambiguity.
I want to abolish the Utils and Commons packages!
All packages must have conceptual cohesion! When and how a package is to be used must be explicit! There must be no ambiguity!
So, for example, if our application has some special way to interact with the CLI, instead of placing it in a namespace ‘Acme/Util/SpecialCli’ we can put it into ‘Acme/App/Infrastructure/Cli/SpecialCli’ which tells us that this package is about the CLI, it is part of the infrastructure of the application ‘App’ of company ‘Acme’. Being part of the App infrastructure also tells us that there is a port, in the application core, to which this package complies to.
Alternatively, if we see this package as something that the language itself should/could have, we can place it in a namespace that reflects that, for example ‘Acme/PhpExtension/SpecialCli’. This tells us that this package should be seen as part of the language itself, and therefore its code should be used directly in the codebase as any language construct. Of course, however, if another company depends on this package it might be wise for them not to depend directly on it, it is safer to create a port/adapter so they can swap it for something else, but if we own the package we can decide to treat it as part of the language as the risk of needing to swap it for another alternative is not that much. It’s always about the tradeoffs.
Another example of something I could consider part of the language would be UUIDs in PHP. I can imagine it is not part of the language because there is a new version every once in a while and it would be a maintainability burden, but otherwise it would be a concept generic, widespread and consistent enough to be part of the language.
So, why not create an implementation of UUIDs and use it as if it was part of PHP itself, just like we use a DateTime object?! As long as we control the implementation, I can see no down side to it.
What about Doctrine Query Language (DQL) ? (Doctrine is a port of Hibernate into PHP) Can we treat DQL as if it was SQL, Elasticsearch QL, or Mongo QL?
So in conclusion, I see these 4 major types of code at the macro level, and I think making them explicit in the code organization can be key for us to stop ending up with a big ball of mud.
What, for me, is an unquestionable truth is that the architecture will always be there, the only question is: are we in control of it or not?!
So let’s explicitly organize the code so it helps communicate the architecture, follow my mental map fully or partially, follow your own mental map, or follow someone else’s mental map, but please let’s use some consistent reasoning to organise the code and make the project explicitly communicate its architecture using its structure, its code organisation.