Shared Kernel as a Package
Code shared between applications within an organization is typically referred to as a shared kernel in domain-driven design. This week's tip discusses this approach and how best to do the sharing.
Sponsor - DevIQ
Thanks to DevIQ for sponsoring this episode! Check out their list of available courses and how-to videos.
Show Notes / Transcript
If you've written more than one application, or worked for a company that has more than one, you've probably shared code between the applications. There are a variety of approaches to this, one of the most awful being the One Solution To Rule Them All approach, in which every bit of code ever written is added to a single code repository and a single solution file. One or more projects in this solution become the shared projects used by many different applications. The benefit of this approach is that developers can easily view and even debug all of the code possibly used by anything. Changes that might break dependent projects are often discovered quickly. However, if a single project requires an update to shared code, it's not easy to have one project depend on a different version of the shared library than another. Even if you use more than one solution, if you're sharing code between multiple solutions at the file system level, you're probably in this boat.
In Domain-Driven Design, the Shared Kernel is code that more than one bounded context depends on. The contract between the shared kernel code and its dependents is that the shared kernel code doesn't change unless all downstream dependencies agree with the change. Often it's one team maintaining the shared kernel and its dependent projects, in which case this is pretty easy, but in larger organizations there may be an approval process involving several teams. When updates do occur, they should be decoupled from dependencies such that they can pull in the update when they're ready. This enables updating the shared kernel code without having to test and update every downstream dependency immediately.
In .NET, one way to gain the ability to have dependent projects pull in the latest updates to the shared kernel whenever they're ready is to use a Nuget package. Any time an update is made to the shared kernel, its package should be updated and its version updated. For example, you might initially have Acme.SharedKernel version 1.0.0, which two projects reference. Project A needs additional functionality, and it's agreed to place it in the shared kernel. A new package is published, with version 1.0.1. Project A updates its version of the package to require 1.0.1 and is able to be deployed. Project B continues to depend on version 1.0.0 and can continue with development and/or remain deployed using this version. Project B can choose when and how often to update which version of the shared kernel package it uses.
If you follow this approach, there are a few things that you may find helpful. First, use continuous integration for your shared kernel library. When you make updates to it, the automated build should compile it, run tests (yes, it should have tests), update its version number, and publish it. This ensures you have a consistent process, which is important especially when we're talking about deploying versioned packages. Next, you'll want to have a way to share the package between your developers and build machines. One nice thing about Nuget is that any file share can serve as a Nuget server, so at a minimum you can simply drop versioned nupkg files into a particular file share. Alternately, you can use an actual Nuget server, such as one built into Jetbrains TeamCity or VSTS/Azure DevOps. You can use a cloud-based solution like myget, if you prefer. In any case, you simply need a way to distibute your shared, versioned packages.
With these fairly small pieces in place, you should find that you're able to decoupled your shared kernel package from its dependents such that you can make updates to it as required and pull in those updates only as needed by each dependency. You should also find that, being a separate solution with a separate automated build, it's less likely that developers will make cavalier changes to the shared kernel, so it should become more stable by default and should only be updated when truly needed by its downstream dependencies. And of course, you should do whatever you can to minimize the things your shared kernel code depends on, since it's going to be depended on by most of your applications. Keep it lightweight and don't depend on anything from it that you can avoid.