Skip to main content
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

1

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');
2

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)
  });
3

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

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()
});
Request contexts, user sessions, and transaction managers should be scoped:
container.register(RequestContextToken, {
  lifetime: 'scoped',
  factory: () => ({ requestId: generateId() })
});
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

Build docs developers (and LLMs) love