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:
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

Build docs developers (and LLMs) love