go-go-scope provides a powerful dependency injection system that integrates with structured concurrency, supporting service lifetimes, auto-wiring, and automatic cleanup.
Overview
The DI system provides:
Type-safe service registration with service tokens
Three service lifetimes : singleton, scoped, transient
Auto-wiring of dependencies
Automatic cleanup when scopes are disposed
Circular dependency detection
Integration with Scope for lifecycle management
Basic usage
Define service tokens
Create type-safe tokens for your services: import { createToken , createContainer } from 'go-go-scope' ;
interface Database {
query ( sql : string ) : Promise < any >;
}
interface UserService {
getUser ( id : string ) : Promise < User >;
}
const DatabaseToken = createToken < Database >( 'Database' );
const UserServiceToken = createToken < UserService >( 'UserService' );
Register services
Register services with their factories and dependencies: const container = createContainer ()
. register ( DatabaseToken , {
lifetime: 'singleton' ,
factory : () => new PostgresDatabase (),
dispose : ( db ) => db . close ()
})
. register ( UserServiceToken , {
lifetime: 'scoped' ,
dependencies: [ DatabaseToken ],
factory : ({ [ DatabaseToken ]: db }) => new UserService ( db )
});
Resolve services in a scope
Services are automatically created and cleaned up: await using s = scope ();
const userService = await container . resolve ( s , UserServiceToken );
const user = await userService . getUser ( '123' );
// Scoped services cleaned up when scope disposed
Service lifetimes
Singleton
Created once and shared across all scopes:
container . register ( DatabaseToken , {
lifetime: 'singleton' ,
factory : () => new Database ()
});
Singleton instances are not automatically disposed. If you need cleanup, register a dispose function.
Scoped
Created once per scope:
container . register ( UserServiceToken , {
lifetime: 'scoped' ,
factory : () => new UserService ()
});
Scoped services are automatically disposed when the scope is disposed.
Transient
Created every time they’re resolved:
container . register ( RequestContextToken , {
lifetime: 'transient' ,
factory : () => ({ id: generateId (), timestamp: Date . now () })
});
Auto-wiring dependencies
Services can declare their dependencies, which are automatically injected:
const CacheToken = createToken < Cache >( 'Cache' );
const DatabaseToken = createToken < Database >( 'Database' );
const UserServiceToken = createToken < UserService >( 'UserService' );
const container = createContainer ()
. register ( CacheToken , {
lifetime: 'singleton' ,
factory : () => new RedisCache ()
})
. register ( DatabaseToken , {
lifetime: 'singleton' ,
factory : () => new PostgresDB ()
})
. register ( UserServiceToken , {
lifetime: 'scoped' ,
dependencies: [ DatabaseToken , CacheToken ],
factory : ({ [ DatabaseToken ]: db , [ CacheToken ]: cache }) =>
new UserService ( db , cache )
});
Modules for organization
Organize related services into modules:
import { createModule } from 'go-go-scope' ;
const databaseModule = createModule (( builder ) => {
builder . register ( DatabaseToken , {
lifetime: 'singleton' ,
factory : () => new PostgresDatabase ()
});
builder . register ( CacheToken , {
lifetime: 'singleton' ,
factory : () => new RedisCache ()
});
});
const serviceModule = createModule (( builder ) => {
builder . register ( UserServiceToken , {
lifetime: 'scoped' ,
dependencies: [ DatabaseToken , CacheToken ],
factory : ({ [ DatabaseToken ]: db , [ CacheToken ]: cache }) =>
new UserService ( db , cache )
});
});
const container = createContainer ()
. use ( databaseModule )
. use ( serviceModule );
Service providers
Create reusable service providers:
import { createServiceProvider } from 'go-go-scope' ;
const provider = createServiceProvider ()
. provide ( DatabaseToken , () => new Database ())
. provide ( UserServiceToken , ( db : Database ) => new UserService ( db ));
await using s = scope ();
const userService = await provider . get ( s , UserServiceToken );
Decorators (experimental)
Use decorators for class-based dependency injection:
import { injectable , inject } from 'go-go-scope' ;
@ injectable ()
class UserService {
constructor (
@ inject ( DatabaseToken ) private db : Database ,
@ inject ( CacheToken ) private cache : Cache
) {}
async getUser ( id : string ) {
const cached = await this . cache . get ( `user: ${ id } ` );
if ( cached ) return cached ;
const user = await this . db . query ( 'SELECT * FROM users WHERE id = ?' , [ id ]);
await this . cache . set ( `user: ${ id } ` , user );
return user ;
}
}
Decorators require TypeScript 5.0+ with experimentalDecorators: true in tsconfig.json.
Integration with Scope
Services integrate seamlessly with scope lifecycle:
await using s = scope ();
// Resolve services
const db = await container . resolve ( s , DatabaseToken );
const userService = await container . resolve ( s , UserServiceToken );
// Use services
const user = await userService . getUser ( '123' );
// Automatic cleanup when scope disposed
Scoped services are automatically registered with the scope’s disposal queue.
Best practices
Prefer singleton for expensive resources
Database connections, HTTP clients, and other expensive resources should be singletons: container . register ( DatabaseToken , {
lifetime: 'singleton' ,
factory : () => createConnection ( process . env . DATABASE_URL ),
dispose : ( db ) => db . close ()
});
Use scoped for request-specific state
Request contexts, user sessions, and transaction managers should be scoped: container . register ( RequestContextToken , {
lifetime: 'scoped' ,
factory : () => ({ requestId: generateId () })
});
Avoid circular dependencies
The DI system detects circular dependencies and throws an error. Refactor to break cycles: // Bad - circular dependency
A depends on B
B depends on A
// Good - extract shared logic
A depends on C
B depends on C
Scopes and tasks Learn about scope lifecycle
Automatic cleanup Understand resource disposal