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:
import type { MedusaContainer } from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"
function myFunction(container: MedusaContainer) {
// Resolve a module service
const productModule = container.resolve(Modules.PRODUCT)
// Resolve a custom service
const myService = container.resolve("myService")
// Resolve with error handling
const optionalService = container.resolve("optional", {
allowUnregistered: true
})
}
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:
import { Module, Modules } from "@medusajs/framework/utils"
import { ApiKeyModuleService } from "@services"
export default Module(Modules.API_KEY, {
service: ApiKeyModuleService,
})
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:
import { asClass, asFunction, asValue } from "@medusajs/deps/awilix"
import type { MedusaContainer } from "@medusajs/framework/types"
import { MyService } from "./services/my-service"
export default async function myLoader(
container: MedusaContainer
) {
// Register as class (new instance per resolution)
container.register({
myService: asClass(MyService).singleton(),
})
// Register as function factory
container.register({
myFactory: asFunction((container) => {
return container.resolve("dependency")
}).singleton(),
})
// Register as value (constant)
container.register({
myConfig: asValue({
apiKey: process.env.API_KEY,
}),
})
}
Registration Modes
Awilix provides three registration modes:
asClass
Registers a class that will be instantiated:
import { asClass } from "@medusajs/deps/awilix"
container.register({
myService: asClass(MyService).singleton(),
})
// Equivalent to: new MyService(dependencies)
asFunction
Registers a factory function:
import { asFunction } from "@medusajs/deps/awilix"
container.register({
myFactory: asFunction((cradle) => {
return new MyService(cradle.dependency)
}).scoped(),
})
asValue
Registers a constant value:
import { asValue } from "@medusajs/deps/awilix"
container.register({
logger: asValue(console),
config: asValue({ apiUrl: "https://api.example.com" }),
})
Lifetime Management
Control how long instances live:
Singleton (Default)
One instance for the entire application:
container.register({
myService: asClass(MyService).singleton(),
})
Use singleton for stateless services, repositories, and module services. This is the most common lifetime.
Scoped
New instance per scope (request):
container.register({
requestService: asClass(RequestService).scoped(),
})
Scoped instances are created once per request scope. Use for services that maintain request-specific state.
Transient
New instance every time:
container.register({
temporaryService: asClass(TemporaryService).transient(),
})
Constructor Injection
Services receive dependencies through their constructor:
import type {
DAL,
InternalModuleDeclaration,
ModulesSdkTypes,
} from "@medusajs/framework/types"
import { MedusaService } from "@medusajs/framework/utils"
import { ApiKey } from "@models"
type InjectedDependencies = {
baseRepository: DAL.RepositoryService
apiKeyService: ModulesSdkTypes.IMedusaInternalService<any>
}
export class ApiKeyModuleService
extends MedusaService<{
ApiKey: { dto: ApiKeyTypes.ApiKeyDTO }
}>({ ApiKey })
{
protected baseRepository_: DAL.RepositoryService
protected readonly apiKeyService_: ModulesSdkTypes.IMedusaInternalService
constructor(
{ baseRepository, apiKeyService }: InjectedDependencies,
protected readonly moduleDeclaration: InternalModuleDeclaration
) {
super(...arguments)
this.baseRepository_ = baseRepository
this.apiKeyService_ = apiKeyService
}
}
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:
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"
import type { IApiKeyModuleService } from "@medusajs/framework/types"
export const createApiKeysStep = createStep(
"create-api-keys",
async (data: CreateApiKeysStepInput, { container }) => {
const service = container.resolve<IApiKeyModuleService>(Modules.API_KEY)
const created = await service.createApiKeys(data.api_keys)
return new StepResponse(created)
}
)
In API Routes
API routes access the container via req.scope:
import type {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
import { Modules } from "@medusajs/framework/utils"
export const GET = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse
) => {
const productModule = req.scope.resolve(Modules.PRODUCT)
const products = await productModule.listProducts()
res.json({ products })
}
In Subscribers
Event subscribers receive the container:
import type {
SubscriberArgs,
SubscriberConfig,
} from "@medusajs/framework"
import { Modules } from "@medusajs/framework/utils"
export default async function productCreatedHandler({
event: { data },
container,
}: SubscriberArgs<{ id: string }>) {
const productModule = container.resolve(Modules.PRODUCT)
const product = await productModule.retrieveProduct(data.id)
// Handle product created event
}
export const config: SubscriberConfig = {
event: "product.created",
}
Container Registration Keys
Medusa provides constants for common services:
import { ContainerRegistrationKeys, Modules } from "@medusajs/framework/utils"
// Module services
const productModule = container.resolve(Modules.PRODUCT)
const orderModule = container.resolve(Modules.ORDER)
// Core services
const logger = container.resolve(ContainerRegistrationKeys.LOGGER)
const query = container.resolve(ContainerRegistrationKeys.QUERY)
const remoteQuery = container.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
const remoteLink = container.resolve(ContainerRegistrationKeys.REMOTE_LINK)
Modules.PRODUCT
Modules.ORDER
Modules.CART
Modules.PAYMENT
Modules.CUSTOMER
Modules.API_KEY
// ... 30+ more
Scoped Containers
Create child containers for isolated scopes:
import type { MedusaContainer } from "@medusajs/framework/types"
import { asValue } from "@medusajs/deps/awilix"
function handleRequest(
parentContainer: MedusaContainer,
userId: string
) {
// Create a scoped container
const scopedContainer = parentContainer.createScope()
// Register request-specific values
scopedContainer.register({
currentUser: asValue({ id: userId }),
})
// Use scoped container
const service = scopedContainer.resolve("myService")
// Clean up (if needed)
scopedContainer.dispose()
}
Testing with DI
DI makes testing easier by allowing mock dependencies:
import { createContainer, asClass, asValue } from "@medusajs/deps/awilix"
import { MyService } from "../services/my-service"
describe("MyService", () => {
let container
let service
beforeEach(() => {
container = createContainer()
// Register mocks
container.register({
logger: asValue({
info: jest.fn(),
error: jest.fn(),
}),
productModule: asValue({
listProducts: jest.fn().mockResolvedValue([]),
}),
})
// Register service under test
container.register({
myService: asClass(MyService).singleton(),
})
service = container.resolve("myService")
})
it("should do something", async () => {
await service.doSomething()
expect(container.resolve("logger").info).toHaveBeenCalled()
})
})
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
type InjectedDependencies = {
baseRepository: DAL.RepositoryService
productService: ModulesSdkTypes.IMedusaInternalService
productVariantService: ModulesSdkTypes.IMedusaInternalService
productCategoryService: ProductCategoryService
[Modules.EVENT_BUS]?: IEventBusModuleService
}
export default class ProductModuleService extends MedusaService {
protected baseRepository_: DAL.RepositoryService
protected readonly productService_: ModulesSdkTypes.IMedusaInternalService
protected readonly productVariantService_: ModulesSdkTypes.IMedusaInternalService
protected readonly productCategoryService_: ProductCategoryService
protected readonly eventBusModuleService_?: IEventBusModuleService
constructor(
{
baseRepository,
productService,
productVariantService,
productCategoryService,
[Modules.EVENT_BUS]: eventBusModuleService,
}: InjectedDependencies,
protected readonly moduleDeclaration: InternalModuleDeclaration
) {
super(...arguments)
this.baseRepository_ = baseRepository
this.productService_ = productService
this.productVariantService_ = productVariantService
this.productCategoryService_ = productCategoryService
this.eventBusModuleService_ = eventBusModuleService
}
}
Source: packages/modules/product/src/services/product-module-service.ts:155-190
Optional Dependencies
Mark optional dependencies with ?:
type InjectedDependencies = {
logger: Logger
cache?: ICacheService // Optional
}
class MyService {
protected logger_: Logger
protected cache_?: ICacheService
constructor({ logger, cache }: InjectedDependencies) {
this.logger_ = logger
this.cache_ = cache
}
async getData(key: string) {
// Use cache if available
if (this.cache_) {
const cached = await this.cache_.get(key)
if (cached) return cached
}
// Fallback to direct fetch
return await this.fetchData(key)
}
}
Next Steps
Services
Learn how to build services with DI
Modules
Understand Medusa’s module architecture