Spraxium logoSpraxium

Providers and Lifecycle

Providers are the injectable services that hold your bot's runtime behavior and business logic. Learn the provider model, constructor injection, the three lifecycle hooks, and how to share providers across modules.

What is a provider

A provider is any class decorated with @Injectable() and registered in a module's providers array. Providers hold your bot's runtime behavior, including database queries, moderation logic, caching, API calls, and anything else that needs to be shared across listeners or other providers. The DI container creates one instance of each provider per module and wires dependencies automatically through constructor parameters, so you never need to manage object creation or dependency graphs manually.

Never instantiate providers with the new keyword. Declare dependencies as constructor parameters and the framework injects them automatically.

import { Injectable } from '@spraxium/common';

@Injectable()
export class MetricsService {
  // business logic lives here
}

Constructor injection

Add dependencies as constructor parameters and Spraxium resolves the graph automatically. Circular dependencies are detected at startup with a clear error before the bot connects.

src/modules/metrics/metrics.service.ts
import { Injectable } from '@spraxium/common';

@Injectable()
export class MetricsRepository {
  async connect(): Promise<void> {}
  async close(): Promise<void> {}
}

@Injectable()
export class MetricsService {
  constructor(private readonly repo: MetricsRepository) {}
}

Avoid heavy I/O in constructors; database connections, cache warming, and external API calls belong in lifecycle hooks.

Keep constructors for dependency wiring only

Never run I/O, database connections, cache warming, or external API calls, inside a constructor. The constructor's sole role is to declare what the class needs. All initialization logic belongs in SpraxiumOnBoot, where the execution stage is explicit and failures are captured cleanly by the lifecycle system.

Lifecycle hooks

Spraxium exposes three lifecycle interfaces for reacting to different stages of the application runtime.

SpraxiumOnBoot

This hook runs after the DI graph is fully instantiated but before the Discord login happens. Use it for startup work that does not require a connected Discord client, such as connecting to a database, validating external configuration, or pre-warming a cache with data from an external API.

SpraxiumOnReady

This hook runs after the Discord ready event fires and the client is fully connected. Use it for any logic that depends on an active Discord client, such as fetching guild data, syncing application commands, or starting scheduled presence updates.

SpraxiumOnShutdown

This hook runs when the process receives a termination signal. Use it for cleanup tasks like closing database connections, flushing write buffers, and cancelling active timers. Keep shutdown logic fast and idempotent so the process exits cleanly under time pressure.

src/modules/metrics/metrics.service.ts
import {
  Injectable,
  type SpraxiumOnBoot,
  type SpraxiumOnReady,
  type SpraxiumOnShutdown,
} from '@spraxium/common';
import type { Client } from 'discord.js';
import { Logger } from '@spraxium/core';

@Injectable()
export class MetricsService
  implements SpraxiumOnBoot, SpraxiumOnReady, SpraxiumOnShutdown {
  private readonly logger = new Logger(MetricsService.name);

  async onBoot(): Promise<void> {
    this.logger.info('Preparing metrics resources...');
  }

  async onReady(client: Client<true>): Promise<void> {
    this.logger.info(`Bot ready in ${client.guilds.cache.size} guilds.`);
  }

  async onShutdown(): Promise<void> {
    this.logger.info('Releasing metrics resources...');
  }
}

You do not need to implement all three hooks in every provider. Implement only the ones that match your provider's actual responsibilities, keeping each class focused on its own concerns. A service that only needs startup initialization implements SpraxiumOnBoot alone, a service that only needs cleanup implements SpraxiumOnShutdown alone, and you should only add SpraxiumOnReady when your logic truly depends on the Discord client being in a connected and ready state.

Sharing providers across modules

By default, a provider is scoped to its own module, meaning other modules cannot inject it unless the owning module explicitly exports it. To share a provider, add it to the module's exports array and import that module wherever the provider is needed. This explicit sharing model keeps dependencies visible and prevents hidden coupling between modules that would otherwise be difficult to trace during debugging or refactoring.

src/modules/metrics/metrics.module.ts
@Module({
  providers: [MetricsService, MetricsRepository],
  exports: [MetricsService],
})
export class MetricsModule {}

@Module({
  imports: [MetricsModule],
  listeners: [EventsListener],
})
export class EventsModule {}

For services that are genuinely needed throughout the entire project, such as logging, configuration, and analytics, use @Global() on the module so its exports become available without explicit imports. Use global modules sparingly because they bypass encapsulation and make dependency tracking harder, so reserve them exclusively for cross-cutting infrastructure that every module in the application truly needs access to.