Skip to main content

Dependency Injection

Medusa uses Awilix as its dependency injection (DI) container to manage service dependencies, promote loose coupling, and enable testability. The container automatically resolves and injects dependencies when services are accessed.

What is Dependency Injection?

Dependency Injection is a design pattern where objects receive their dependencies from an external source rather than creating them internally. This enables:
  • Loose coupling - Services don’t depend on concrete implementations
  • Testability - Dependencies can be mocked or stubbed
  • Modularity - Services can be swapped or extended
  • Lifecycle management - Container controls object creation and disposal
Medusa’s DI container is based on Awilix, a powerful IoC container for Node.js that supports constructor injection, lifetime management, and automatic resolution.

The Container

The Medusa container is an Awilix container that holds all registered services, modules, and dependencies:
Source: packages/core/core-flows/src/api-key/steps/create-api-keys.ts:35-36

Service Registration

Automatic Registration

Medusa automatically registers:
  • Module services - All module services from packages/modules/
  • Core services - Framework services (logger, event bus, etc.)
  • Custom services - Services in your src/services/ directory

Module Registration

Modules self-register using the Module factory:
This registers ApiKeyModuleService under the key Modules.API_KEY (which equals "apiKey"). Source: packages/modules/api-key/src/index.ts

Manual Registration

Register services manually in loaders:

Registration Modes

Awilix provides three registration modes:

asClass

Registers a class that will be instantiated:

asFunction

Registers a factory function:

asValue

Registers a constant value:

Lifetime Management

Control how long instances live:

Singleton (Default)

One instance for the entire application:
Use singleton for stateless services, repositories, and module services. This is the most common lifetime.

Scoped

New instance per scope (request):
Scoped instances are created once per request scope. Use for services that maintain request-specific state.

Transient

New instance every time:

Constructor Injection

Services receive dependencies through their constructor:
Source: packages/modules/api-key/src/services/api-key-module-service.ts:39-63
Constructor parameter names must match the registration keys in the container. Use destructuring to make dependencies explicit.

Resolution Patterns

In Workflow Steps

Steps receive the container in their context:

In API Routes

API routes access the container via req.scope:

In Subscribers

Event subscribers receive the container:

Container Registration Keys

Medusa provides constants for common services:

Scoped Containers

Create child containers for isolated scopes:

Testing with DI

DI makes testing easier by allowing mock dependencies:

Best Practices

Use Constructor Injection

Always inject dependencies through the constructor, not properties.

Depend on Interfaces

Type dependencies with interfaces, not concrete classes.

Prefer Singleton

Use singleton lifetime for stateless services to reduce memory.

Avoid Container in Services

Don’t inject the container itself - inject specific dependencies.
Avoiding injecting the entire container into services. This creates a service locator anti-pattern and hides dependencies. Always inject specific services.

Common Patterns

Service with Multiple Dependencies

Source: packages/modules/product/src/services/product-module-service.ts:155-190

Optional Dependencies

Mark optional dependencies with ?:

Next Steps

Services

Learn how to build services with DI

Modules

Understand Medusa’s module architecture