The IoC (Inversion of Control) container is the heart of AdonisJS’s dependency injection system. It manages service instances, resolves dependencies automatically, and promotes loose coupling in your applications.
What is the IoC Container?
The IoC container is a service registry that:
Stores bindings for services and their factories
Resolves dependencies automatically through constructor injection
Manages singleton and transient lifecycles
Enables testability through dependency injection
The AdonisJS container is powered by @adonisjs/fold , a standalone dependency injection library.
Basic Usage
Accessing the Container
The container is available on the application instance:
import app from '@adonisjs/core/services/app'
// Resolve a service
const logger = await app . container . make ( 'logger' )
logger . info ( 'Hello from the container' )
Making Instances
Use the make() method to resolve services from the container:
// By string key
const emitter = await app . container . make ( 'emitter' )
// By class reference
import { Logger } from '@adonisjs/core/logger'
const logger = await app . container . make ( Logger )
Container Bindings
Bindings define how services are created and managed. AdonisJS supports several types of bindings:
Singleton Bindings
Singletons are created once and reused throughout the application:
import type { ApplicationService } from '@adonisjs/core/types'
export default class MyProvider {
constructor ( protected app : ApplicationService ) {}
register () {
this . app . container . singleton ( 'cache' , () => {
return new CacheManager ({
driver: 'redis' ,
connection: {
host: '127.0.0.1' ,
port: 6379
}
})
})
}
}
Every call to app.container.make('cache') returns the same instance.
Transient Bindings
Transient bindings create a new instance on each resolution:
register () {
this . app . container . bind ( 'notification' , () => {
return new NotificationService ()
})
}
Every call to app.container.make('notification') returns a new instance.
Value Bindings
Bind a direct value without a factory:
register () {
this . app . container . bindValue ( 'apiUrl' , 'https://api.example.com' )
this . app . container . bindValue ( 'apiKey' , process . env . API_KEY )
}
Class Bindings
Bind by class constructor for type-safe resolution:
import { UserService } from '#services/user_service'
register () {
this . app . container . singleton ( UserService , () => {
return new UserService ( dependencies )
})
}
// Resolve using class reference
const userService = await app . container . make ( UserService )
Aliasing
Create multiple ways to access the same binding:
register () {
this . app . container . singleton ( 'database' , () => {
return new DatabaseManager ( config )
})
// Create alias
this . app . container . alias ( 'db' , 'database' )
// Both work now:
const database = await app . container . make ( 'database' )
const db = await app . container . make ( 'db' )
// database === db (same instance)
}
Dependency Injection
The container can automatically inject dependencies into class constructors:
Constructor Injection
Use the @inject() decorator to enable automatic dependency resolution:
import { inject } from '@adonisjs/core'
import type { HttpContext } from '@adonisjs/core/http'
import UserService from '#services/user_service'
import MailService from '#services/mail_service'
@ inject ()
export default class UsersController {
constructor (
protected userService : UserService ,
protected mailService : MailService
) {}
async store ({ request , response } : HttpContext ) {
const data = request . only ([ 'email' , 'password' ])
const user = await this . userService . create ( data )
await this . mailService . send ( 'welcome' , { user })
return response . created ( user )
}
}
The @inject() decorator analyzes the constructor parameters and automatically resolves them from the container.
Manual Injection
Specify exactly what to inject using injection tokens:
import { inject } from '@adonisjs/core'
@ inject ([ 'logger' , 'cache' ])
export default class MyService {
constructor (
protected logger : LoggerService ,
protected cache : CacheService
) {}
}
Contextual Binding
Provide different implementations based on the context:
register () {
// Default binding
this . app . container . singleton ( 'storage' , () => {
return new LocalStorage ()
})
// Context-specific binding
this . app . container . when ( MediaController )
. provide ( 'storage' , () => {
return new S3Storage ()
})
}
Resolver
The resolver is passed to binding factories and provides access to the container:
register () {
this . app . container . singleton ( 'repository' , async ( resolver ) => {
// Resolve dependencies from the container
const database = await resolver . make ( 'database' )
const logger = await resolver . make ( 'logger' )
return new UserRepository ( database , logger )
})
}
Container Services
AdonisJS registers several core services in the container:
app - Application Instance
The main application instance: const app = await container . make ( 'app' )
console . log ( app . getState ())
console . log ( app . environment )
The logger manager for logging operations: const logger = await container . make ( 'logger' )
logger . info ( 'Application started' )
logger . error ({ err: error }, 'Request failed' )
Access to application configuration: const config = await container . make ( 'config' )
const appKey = config . get ( 'app.appKey' )
The event emitter for application events: const emitter = await container . make ( 'emitter' )
emitter . emit ( 'user:created' , { userId: 123 })
The HTTP router instance: const router = await container . make ( 'router' )
router . get ( '/users' , [ UsersController , 'index' ])
The HTTP server instance: const server = await container . make ( 'server' )
await server . boot ()
encryption - Encryption Service
Encryption and decryption functionality: const encryption = await container . make ( 'encryption' )
const encrypted = encryption . encrypt ( 'secret data' )
Password hashing service: const hash = await container . make ( 'hash' )
const hashed = await hash . make ( 'password' )
The Ace CLI kernel: const ace = await container . make ( 'ace' )
await ace . exec ( 'make:controller' , [ 'UserController' ])
testUtils - Test Utilities
Utilities for testing: const testUtils = await container . make ( 'testUtils' )
const ctx = testUtils . createHttpContext ()
Type Safety
AdonisJS provides full TypeScript support for container bindings:
Declaring Bindings
Extend the ContainerBindings interface to add type information:
import UserService from '#services/user_service'
import CacheService from '#services/cache_service'
declare module '@adonisjs/core/types' {
interface ContainerBindings {
userService : UserService
cache : CacheService
}
}
Now the container knows about these types:
// TypeScript knows this returns UserService
const userService = await app . container . make ( 'userService' )
// Type error: 'unknown' is not a valid binding
const foo = await app . container . make ( 'unknown' )
Type-Safe Resolution
The container provides full IntelliSense and type checking:
// ✅ Type-safe
const logger = await app . container . make ( 'logger' )
logger . info ( 'Hello' ) // TypeScript knows logger methods
// ✅ Class-based resolution is also type-safe
import { Logger } from '@adonisjs/core/logger'
const logger2 = await app . container . make ( Logger )
Advanced Patterns
Factory Pattern
Create factories for complex object creation:
register () {
this . app . container . singleton ( 'httpClientFactory' , () => {
return ( baseURL : string ) => {
return new HttpClient ({
baseURL ,
timeout: 5000 ,
headers: { 'User-Agent' : 'MyApp' }
})
}
})
}
// Usage
const factory = await app . container . make ( 'httpClientFactory' )
const githubClient = factory ( 'https://api.github.com' )
const twitterClient = factory ( 'https://api.twitter.com' )
Manager Pattern
Implement the manager pattern for multi-driver services:
import { Manager } from '@adonisjs/core/manager'
export class CacheManager extends Manager < CacheDriver > {
protected createRedisDriver () {
return new RedisCache ( this . config . stores . redis )
}
protected createMemoryDriver () {
return new MemoryCache ( this . config . stores . memory )
}
}
register () {
this . app . container . singleton ( 'cache' , () => {
return new CacheManager ( config )
})
}
// Usage
const cache = await app . container . make ( 'cache' )
const redis = cache . use ( 'redis' )
const memory = cache . use ( 'memory' )
Lazy Services
Defer expensive service creation until first access:
register () {
this . app . container . singleton ( 'search' , async () => {
// Heavy import only loaded when needed
const { ElasticSearch } = await import ( 'elasticsearch' )
return new ElasticSearch ( config )
})
}
Container Events
The container emits events when bindings are resolved:
import app from '@adonisjs/core/services/app'
const emitter = await app . container . make ( 'emitter' )
emitter . on ( 'container_binding:resolved' , ({ binding , value }) => {
console . log ( `Resolved: ${ binding } ` )
console . log ( 'Instance:' , value )
})
Swapping Bindings
Replace bindings for testing or different environments:
// Original binding
register () {
this . app . container . singleton ( 'mailer' , () => {
return new SmtpMailer ( config )
})
}
// Swap in tests
import { test } from '@japa/runner'
test ( 'send email' , async ({ assert }) => {
const app = createApp ()
// Replace with mock
app . container . swap ( 'mailer' , () => {
return new MockMailer ()
})
const mailer = await app . container . make ( 'mailer' )
assert . instanceOf ( mailer , MockMailer )
})
Hooks
Register hooks that run when bindings are resolved:
register () {
this . app . container . singleton ( 'database' , () => {
return new Database ( config )
})
// Hook runs after resolution
this . app . container . resolving ( 'database' , ( database ) => {
database . on ( 'query' , ( sql ) => {
console . log ( 'SQL:' , sql )
})
})
}
Checking Bindings
Check if a binding exists before resolving:
if ( app . container . hasBinding ( 'lucid.db' )) {
const db = await app . container . make ( 'lucid.db' )
// Use database
}
Best practices for optimal container performance:
Use singletons for stateless services - Services like loggers, routers, and managers
Use transient bindings for stateful services - Services that maintain request-specific state
Lazy load heavy dependencies - Use dynamic imports in factory functions
Cache resolved instances - Store frequently used services in variables
Avoid circular dependencies - Design services to depend on abstractions, not implementations
Common Patterns in Practice
Repository Pattern
import { inject } from '@adonisjs/core'
import Database from '@adonisjs/lucid/services/db'
@ inject ()
export default class UserRepository {
constructor ( protected db : Database ) {}
async find ( id : number ) {
return this . db . from ( 'users' ). where ( 'id' , id ). first ()
}
async create ( data : any ) {
return this . db . table ( 'users' ). insert ( data )
}
}
Service Layer
import { inject } from '@adonisjs/core'
import UserRepository from '#repositories/user_repository'
import HashService from '#services/hash_service'
@ inject ()
export default class AuthService {
constructor (
protected userRepo : UserRepository ,
protected hash : HashService
) {}
async login ( email : string , password : string ) {
const user = await this . userRepo . findByEmail ( email )
if ( ! user || ! ( await this . hash . verify ( user . password , password ))) {
throw new Error ( 'Invalid credentials' )
}
return user
}
}
Controller Injection
import { inject } from '@adonisjs/core'
import type { HttpContext } from '@adonisjs/core/http'
import AuthService from '#services/auth_service'
@ inject ()
export default class AuthController {
constructor ( protected authService : AuthService ) {}
async login ({ request , response , auth } : HttpContext ) {
const { email , password } = request . only ([ 'email' , 'password' ])
const user = await this . authService . login ( email , password )
await auth . use ( 'web' ). login ( user )
return response . redirect ( '/dashboard' )
}
}
Next Steps
Service Providers Learn how to register services with the container
Application Lifecycle Understand when services are created and resolved