A powerful and lightweight module composition strategy responsible for orchestrating your logic, enabling you to easily respect SOLID principles within your infinitely scalable app.
When dealing with a small or large scale applications, you usually have groups of logic that need to be separated in order for them to be re-usable. For example you have a module for handling api requests, one for communicating with the database, one for sending emails, you get the idea.
These modules need to be able to have an initialisation phase and provide a way for other modules to depend on them and even extend them.
All modules are grouped inside the
Kernel and we call them
bundles. A bundle is a group of logic that is re-usable.
The Kernel is nothing without bundles. Bundles contain the logic.
You can add the bundle to the
kernel in the constructor or later on:
Initialisation process prepares and initialiases all the bundles registered inside it. You can regard your
Bundles as groups of independent logic or strongly separated concerns.
This design pattern solves the problem with modifying or extending logic of other bundles.
kernel contains a group
container contains references to instances, strings (passwords, api keys), etc.
An oversimplification of D.I. is that you don't depend on "real" implementations, you depend on references (strings, classes, tokens). For example, let's say you have a container that contains everything you need in your app, connection to the database, credentials, anything. And let's say you want to access the database service to do an insert, so instead of getting the service directly (by
new Service()-ing it, or accessing the singleton
Service.doSomething()), you use the container:
Now let's say the
databaseService needs some credentials and a host to connect to. So instead of using a string directly or reading directly from env, it reads it from container:
Now when we
.get() MyDatabaseService from the
container, it will automatically inject the dependencies.
In conclusion, we never instantiate via
new we only fetch instances of our services through the container, and there's only one container which is provided by the
Kernel (accessible via
Bundle methods). Above we showed how to use references as strings (
database_service) but references can be classes or tokens.
We recommend that you use tokens or classes and avoid strings, they have been shown here to illustrate the idea.
We regard as a
Service a class that executes logic.
Now let's use them:
Services are singletons, meaning it instantiates only once:
If you want to avoid having strings collide, you should use tokens as references:
You can also specify a list of parameters to the kernel. When the bundles within your app need the same information, you should use this instead of passing it as a config to each bundle.
You can inject parameters from kernel, or others like this:
To benefit of autocompletion for your kernel parameters:
By default the available parameters are:
This technique allows us to have typesafety for Event Management, and event handlers can be async and can be blocking for the event propagation.
Note that you also have
removeGlobalListener, also you can set the order in which the handlers are executed:
You can also add a filter to the option, that will only allow certain "instances" of events. Let's say everytime you insert an object into the database you emit an event that contains also the collectionName in it. And you would like to listen to events for a certain collection:
This is just a shorthand function so it allows your handler to focus on the task at hand rather than conditioning execution.
In order to listen to events we have to register them somehow. This is why we introduce the concept of "warmup" for listeners.
All listeners must be warmed up for them to work.
Warming up instantiates the specific Service, and if the
init() function exists it will be called.
For example, you might use this for a DatabaseConnection, you want to immediately connect and you implement this in the service's
Ok, now that you've learned the basics of containers and async event management, it's time to understand where all logic lies (inside the bundles and their services)
Bundles can have a specific configuration to them and this is passed when instantiating them:
You can also specify a default configuration for your bundle. The config you pass when constructing the bundle gets merged deeply with
Another feature regarding configuration is providing a required config. A config that you must always pass:
We decided to make this split because we want developers to force a specific value for a bundle that wouldn't be feasible to have it in
You can also have more complex validation logic via
Right now you've seen that bundles get initialised via the
init() async function. But there's more to it because we wanted to allow bundles to work together and extend each other.
Kernel also emits the following events (name descriptive enough), and listeners are run in-sync:
So, in theory you have the chance to hook even more to the bundles you love:
Let's say we have a bundle that needs an API key, for example,
MailBundle needs some authentication parameters. The way we connect Bundle's config to the container is by setting some constants into the container which the services use in their instantiation.
It's nice to never rely on string matching to see which exception was thrown, and it's nice to have typesafety as well. We recommend you always use this instead of the standard
Error. The reason we changed the name to
Exception instead of Error was to avoid confusion that these class would somehow extend the
Keep your bundle easily modifiable by allowing injection of customised services. The strategy is to use an
abstract class as a placeholder, but there are other solutions as well.
When would you like to do this?
This would be suited when you expose a bundle in which you allow a certain service to be overriden.
Let's think of a bundle that does some security thingies and they want to allow you to inject a custom hash function.
This strategy is to explicitly state which hasher you want in the constructor, but in real-life scenarios, you'll most likely do this inside your own
This strategy may feel a bit obscure as you allow any bundle to modify the config at any stage, if you want to prevent such things happening to your bundle, you can do something like:
If you want to have more control over the
setHasher you can use
bundle.phase to ensure that it is set within the preparation or initialisation phase.
We recommend using jest for testing. The idea here is that when you have a
kernel with multiple bundles, sometimes your bundles might behave differently, this is why we have a kernel parameter called
When testing the full kernel you need to have an ecosystem creation function. We recommend having a separate
kernel.test.ts file where you instantiate the kernel.
Ensure that the code above is loaded before all tests. Now you would be able to run your tests:
These set of tools: the
bundles (extendable & hackable), the async event management, the type-safe exceptions allow us to construct high-quality applications which respect the SOLID principles and can be nicely re-used. A good example of how this is put to good use is inside the