The golden rule: High-level modules should not depend on low-level modules. Both should depend on the abstraction. Abstractions should not depend on details. Details should depend on abstractions.
In each stack lambda folder, there will be a dependency injection container initialized. We’re using the awilix tool to manage dependecy injection (https://www.npmjs.com/package/awilix). In the Container.ts folder, you can register any dependencies you want to use as classes, values or functions.
Note that we since dependencies are resolved by their names use the ContainerKeys enum to avoid programming with strings.
// Create the container and set the injectionMode to PROXY (which is also the default).
export const Container = createContainer({
injectionMode: InjectionMode.PROXY,
});
//Repositories
Container.register({
[ContainerKeys.ACCCOUNT_REPOSITORY]: asClass(AccountRepository),
[ContainerKeys.ENVIRONMENT_NAME]: asValue(ENVIRONMENT_NAME),
});
//Models
Container.register({
[ContainerKeys.ACCOUNT_MODEL]: asFunction(
(props: { [ContainerKeys.MODEL_MANAGER]: ModelManager }) => {
const { modelManager } = props;
const result = modelManager.getModel(DynamoTableNames.ACCOUNT_TABLE, AccountSchema);
return result;
}
).inject((container) => {
return {
[ContainerKeys.MODEL_MANAGER]: container.resolve(ContainerKeys.MODEL_MANAGER),
};
}),
We use interfaces as types to be injected so that any class is dependent on interfaces that are resolved by the container at runtime. This ensures SOLID patterns are enforced in our lambda UseCase classes.
//In the Container
//Use the enum to refer to names
[ContainerKeys.SECRETS_REPOSITORY]: asClass(SecretsRepository),
//In the implementing class
export interface ChargebeeRepositoryProps {
//code against the interface
//use the Container key enum
[ContainerKeys.SECRETS_REPOSITORY]: ISecretsRepository;
}
export class ChargebeeRepository extends RequestHelper implements IChargebeeRepository {
private secretsRepository: ISecretsRepository;
constructor(props: ChargebeeRepositoryProps) {
//use the Container key enum and assign to a local variable
this.secretsRepository = props[ContainerKeys.SECRETS_REPOSITORY];
}
}
This is a lambda that gets the logged-in user from the user Id in the session and returns it. Let’s look at the two files.
import { Container } from "../../../dependencyInjection/Container";
import { ContainerKeys } from "../../../dependencyInjection/ContainerKeys";
import { GetMe } from "./GetMe";
export const handler = GetMe({
[ContainerKeys.USER_REPOSITORY]: Container.resolve(ContainerKeys.USER_REPOSITORY),
[ContainerKeys.LOG_MANAGER]: Container.resolve(ContainerKeys.LOG_MANAGER),
[ContainerKeys.ERROR_MANAGER]: Container.resolve(ContainerKeys.ERROR_MANAGER),
[ContainerKeys.SESSION_MANAGER]: Container.resolve(ContainerKeys.SESSION_MANAGER),
});
Let’s break down what’s happening here. The lambda handler only does one thing, it gets instances of all the dependencies of the lambda function and creates a copy of it. This is the only place the container needs to be called.