Service Lifecycle
Every Webda service follows a strict lifecycle managed by Core. Understanding this sequence is essential to writing services that initialize resources correctly and shut down cleanly.
Lifecycle state machine
Lifecycle phases
1. Constructor
Called when the framework instantiates the service. The constructor must call super(app, name, params). Do not do async work here.
export class MyService extends Service<MyServiceParameters> {
protected connection: DatabaseConnection;
constructor(app: Application, name: string, params: any) {
super(app, name, params);
// Synchronous init only — no async, no network calls
}
}
2. resolve(): this
Called after all services are instantiated, before init(). Synchronous — resolve() returns this, not a Promise. Use it to:
- Validate configuration
- Read
@Injectdependencies (already injected by the timeresolve()runs) - Wire up cross-service references that don't require I/O
If you need to do async work, defer it to init().
resolve(): this {
super.resolve(); // always call super first
if (!this.parameters.apiKey) {
throw new Error("apiKey is required in MyService configuration");
}
// At this point @Inject dependencies are already populated
this.log.info("Resolved with store:", this.store.getName());
return this;
}
3. init(): Promise<this>
Called after resolve(). Use init() for async initialization:
- Open database connections
- Start background tasks
- Register event handlers
async init(): Promise<this> {
await super.init();
this.connection = await DatabaseConnection.connect(this.parameters.url);
this.log.info("Connected to database", { url: this.parameters.url });
return this;
}
4. Running
The service is fully initialized. The application handles requests and the service processes them normally.
5. stop(): Promise<void>
Called during graceful shutdown (SIGINT, Core.stop()). Release resources in reverse order of acquisition:
async stop(): Promise<void> {
this.log.info("Shutting down");
// Close resources before calling super
await this.connection.close();
await this.backgroundTask.cancel();
await super.stop(); // call super last
}
Complete example
import { Service, ServiceParameters, Inject } from "@webda/core";
import { Bean } from "@webda/core";
import { useLog } from "@webda/workout";
interface EmailServiceParameters extends ServiceParameters {
smtpHost: string;
smtpPort: number;
from: string;
}
@Bean
export class EmailService extends Service<EmailServiceParameters> {
protected log = useLog("EmailService");
protected transport: any;
resolve(): this {
super.resolve();
if (!this.parameters.smtpHost) {
throw new Error("smtpHost is required");
}
this.log.info("Configuration validated", {
host: this.parameters.smtpHost,
port: this.parameters.smtpPort
});
return this;
}
async init(): Promise<this> {
await super.init();
// Create SMTP transport
this.transport = createTransport({
host: this.parameters.smtpHost,
port: this.parameters.smtpPort ?? 587
});
this.log.info("SMTP transport initialized");
return this;
}
async stop(): Promise<void> {
this.log.info("Closing SMTP transport");
this.transport?.close();
await super.stop();
}
async sendEmail(to: string, subject: string, body: string): Promise<void> {
await this.transport.sendMail({
from: this.parameters.from,
to,
subject,
html: body
});
}
}
Sequence diagram
Key rules
- Always call
super.resolve()first inresolve()(synchronous). - Always call
await super.init()first ininit(). - Call
await super.stop()last instop()(reverse order). - Never do async work in
resolve()—resolve()is sync. Defer async work toinit(). - Never do async work in the constructor — use
init(). - Never access
@Injectdependencies in the constructor — they are not yet populated. Access them inresolve()or later.
Verify
cd packages/core
pnpm test
✓ packages/core — service lifecycle tests pass
See also
- Architecture — how
Coreorchestrates the lifecycle - Services —
Servicebase class,@Bean,@Inject - Events —
Webda.Initand other lifecycle events