Skip to main content

Dependency Injection

Entities in the domain typically have relationships, and to describe any user scenario, you need to operate with multiple entities. To solve this task, the framework has a built-in DI container that allows you to declaratively describe and resolve dependencies between services.

Dependencies on Services

In our application, we already have user and order entities. Let's imagine that when making a purchase, the user earns bonuses, and we want to update information about their current bonus count after the purchase.

Let's add the corresponding method to the user service:

class UserService extends Service {
@operation
*getBonuses() {
return yield* call(fetchBonuses);
}
}

Now we need to update bonus information after placing an order, but this logic is in different services. Let's indicate that OrderService depends on UserService. For this, we'll use the inject decorator.

class OrderService extends Service {
#userService: UserService

constructor(
@inject(OperationService) operationService: OperationService,
@inject(UserService) userService: UserService
) {
// mandatory base class initialization
super(operationService)
this.#userService = userService;
}

@daemon()
@operation({
id: ORDERS_OPERATION_ID,
updateStrategy: appendStrategy
})
*addOrder() {
const id = getNewId();
const order = {id, description: `Order ${id}`};

yield* call(addOrder, order);
// update bonus information
yield* call(this.#userService.getBonuses);
return [order];
}
}

Now let's update the interface:

info

Dependencies must be registered in the order they need to be resolved. In our example, the dependency is UserService => OrderService, so we register them in that order.

Dependencies on Custom Classes

Besides services, you can inject any class by inheriting it from the base Dependency class.

Let's look at an example of when this might be useful. In our application, we need to make requests to the backend — currently this is described as a set of utilities fetchUser, fetchOrders, etc. This approach doesn't provide any explicit contract, makes backend work harder to test and configure — let's fix this.

Let's create an explicit contract for the API as a class.

class API extends Dependency {
toString() {
return 'API'
}

fetchUser() {};
fetchBonuses();
fetchOrders() {};
addOrder(order) {};
}

Register our class:

function App({children}) {
const di = useDI();

di.registerService(new API())

const userService = di.createService(UserService);
di.registerService(userService)

const orderService = di.createService(OrderService);
di.registerService(orderService)

return (...)
}

Then we can create an explicit dependency of the service on the API:

class UserService extends Service {
#api: API;

toString() {
return "UserService"
}

constructor(
@inject(OperationService) operationService: OperationService,
@inject(API) api: API
) {
super(operationService);
this.#api = api;
}

@operation
*getUserInfo() {
return yield* call(this.#api.fetchUser);
}

@operation
*getBonuses() {
return yield* call(this.#api.fetchBonuses);
}
}

Now our API has an explicit contract, and our services are much easier to test since we can substitute any implementation of the API contract.

Full example:

Dependencies by Key

As a dependency, you can pass not only class instances — you can pass any value by registering it with a key.

For example, applications typically have some context that depends on the environment, such as which backend domain to use in testing vs. production.

In the simplest case, this might look like:

// use the special DependencyKey type that contains meta-information about the dependency
const APP_CONTEXT_KEY = 'APP_CONTEXT' as DependencyKey<AppContext>;

type AppContext = {
env: string;
}

const appContext: AppContext = {
env: "testing"
}
class API extends Dependency {
#host: string;

constructor(@inject(APP_CONTEXT_KEY) {env}: AppContext) {
this.#host = env === "testing" ? "..." : "..."
}
}

We can make it available to our application by registering it as a dependency by key.

function App({children}) {
const di = useDI();

di.registerDependency(APP_CONTEXT_KEY, appContext);

const api = di.createService(API);
di.registerService(api);

di.unregisterService(UserService)
const userService = di.createService(UserService);
di.registerService(userService)

di.unregisterService(OrderService)
const orderService = di.createService(OrderService);
di.registerService(orderService)

return (...);
}

In the UI, you can also read values of any dependencies:

const di = useDI();
const appContext = di.getDependency(APP_CONTEXT_KEY);

Let's add environment display to the interface: