Architecture Notes
June 25, 2026 ยท View on GitHub
Architecture Notes
Summary
1. Knifecycle
The knifecycle project is intended to be a dependency
injection
with inversion of control
tool. It will always be tied to this goal since I prefer
composing software instead of using frameworks and DI/IC is
a major part to design strong software in my opinion.
It is designed to have a low footprint on services code.
There is nothing worse than having to write specific code for
a given tool. With knifecycle, services can be either constants,
functions or objects created synchronously or asynchronously. They
can be reused elsewhere (even when not using DI) with no changes
at all since they are just simple functions with annotations
set as a property.
In fact, the Knifecycle API is aimed to allow to statically build its services load/unload code once in production.
1.1. OOP
The knifecycle use case is one of the rare use case where
OOP
principles are a good fit.
A service provider is full of state since its concern is precisely to encapsulate your application global states.
1.2. Creating initializers
knifecycle uses initializers at its a core. An initializer is basically
an asynchronous function with some annotations:
- name: it uniquely identifies the initializer so that it can be referred to as another initializer dependency.
- type: an initializer can be of three types at the moment (constant, service or provider). The initializer annotations varies according to those types as we'll see later on.
- injected dependencies: an array of dependencies declarations that declares which initializer this initializer depends on. Constants logically cannot have dependencies.
- options: various options like for example, if the initializer implements the singleton pattern or not.
- value: only used for constant, this property allows to know the value the initializer resolves to without actually executing it.
- extra: an extra property for custom use that will be propagated by the various other decorators you'll find in this library.
Knifecycle provides a set of decorators that allows you to simply
create new initializers.
1.2.1. Dependencies declaration syntax
The dependencies syntax is of the following form:
?serviceName>mappedName
The ? flag indicates an optional dependency.
>mappedName is optional and allows to inject
mappedName as serviceName.
It allows to write generic services with fixed
dependencies and remap their name at injection time.
1.3. Registering initializers
The first step to use knifecycle is to create a new
Knifecycle instance and register the previously
created initializers.
Initializers can be of three types:
- constants: a
constantinitializer resolves to any constant value. - services: a
serviceinitializer directly resolve to the actual service it builds. It can be objects, functions or literal values. - providers: they instead resolve to an object that
contains the service built into the
serviceproperty but also an optionaldisposeproperty exposing a method to properly stop the service and afatalErrorPromisethat will be rejected if an unrecoverable error happens allowing Knifecycle to terminate.
Initializers can be declared as singletons (constants are of course only singletons). This means that they will be instantiated once for all for each executions silos using them (we will cover this topic later on).
1.4. Execution silos
Once every initializers are registered, we need a way to bring
them to life. Execution silos are where the magic happens.
For each call of the run method with given dependencies,
a new silo is created and the required environment to
run the actual code is leveraged.
Depending on your application design, you could run it in only one execution silo or into several ones according to the isolation level your wish to reach.
2. Build
Using Knifecycle only makes sense for monoliths. For some targets like serverless functions, a better approach is to simply build a raw initialization function.
For the build to work, we need:
- a hash of various constants that may be used.
- an autoloader that resolves dependencies names to its actual initializer
- the dependencies list you want to initialize
3. TypeScript tweaks
Sadly TypeScript does not allow to add generic types
in all cases. This is why (Service|Provider)Initializer
types do not embed the (Service|Provider)Properties
directly. Instead, we use this utility function to
reveal it to TypeScript and, by the way, check their
completeness at execution time.
For more details, see: https://stackoverflow.com/questions/64948037/generics-type-loss-while-infering/64950184#64950184