go/docs/lifecycle.md
Snider 89d189dd95 docs: add human-friendly documentation for Core Go framework
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 13:02:37 +00:00

5.1 KiB

title description
Lifecycle Startable and Stoppable interfaces, startup and shutdown ordering.

Lifecycle

Core manages the startup and shutdown of services through two opt-in interfaces. Services implement one or both to participate in the application lifecycle.

Interfaces

Startable

type Startable interface {
    OnStartup(ctx context.Context) error
}

Services implementing Startable have their OnStartup method called during ServiceStartup. This is the place to:

  • Open database connections
  • Register message bus handlers (queries, tasks)
  • Start background workers
  • Validate configuration

Stoppable

type Stoppable interface {
    OnShutdown(ctx context.Context) error
}

Services implementing Stoppable have their OnShutdown method called during ServiceShutdown. This is the place to:

  • Close database connections
  • Flush buffers
  • Save state
  • Cancel background workers

A service can implement both interfaces:

type Service struct{}

func (s *Service) OnStartup(ctx context.Context) error {
    // Initialise resources
    return nil
}

func (s *Service) OnShutdown(ctx context.Context) error {
    // Release resources
    return nil
}

Ordering

Startup: Registration Order

Services are started in the order they were registered. If you register services A, B, C (in that order), their OnStartup methods are called as A, B, C.

Shutdown: Reverse Registration Order

Services are stopped in reverse registration order. If A, B, C were registered, their OnShutdown methods are called as C, B, A.

This ensures that services which depend on earlier services are torn down first.

c, err := core.New()

_ = c.RegisterService("database", dbService)   // started 1st, stopped 3rd
_ = c.RegisterService("cache", cacheService)     // started 2nd, stopped 2nd
_ = c.RegisterService("api", apiService)         // started 3rd, stopped 1st

_ = c.ServiceStartup(ctx, nil)  // database -> cache -> api
_ = c.ServiceShutdown(ctx)      // api -> cache -> database

ServiceStartup

func (c *Core) ServiceStartup(ctx context.Context, options any) error

ServiceStartup does two things, in order:

  1. Calls OnStartup(ctx) on every Startable service, in registration order.
  2. Broadcasts an ActionServiceStartup{} message via the message bus.

If any service returns an error, it is collected but does not prevent other services from starting. All errors are aggregated with errors.Join and returned together.

If the context is cancelled before all services have started, the remaining services are skipped and the context error is included in the aggregate.

ServiceShutdown

func (c *Core) ServiceShutdown(ctx context.Context) error

ServiceShutdown does three things, in order:

  1. Broadcasts an ActionServiceShutdown{} message via the message bus.
  2. Calls OnShutdown(ctx) on every Stoppable service, in reverse registration order.
  3. Waits for any in-flight PerformAsync background tasks to complete (respecting the context deadline).

As with startup, errors are aggregated rather than short-circuiting. If the context is cancelled during shutdown, the remaining services are skipped but the method still waits for background tasks.

Built-in Lifecycle Messages

Core broadcasts two action messages as part of the lifecycle. You can listen for these in any registered action handler:

Message When
ActionServiceStartup{} After all Startable services have been called
ActionServiceShutdown{} Before Stoppable services are called
c.RegisterAction(func(c *core.Core, msg core.Message) error {
    switch msg.(type) {
    case core.ActionServiceStartup:
        // All services are up
    case core.ActionServiceShutdown:
        // Shutdown is beginning
    }
    return nil
})

Error Handling

Lifecycle methods never panic. All errors from individual services are collected via errors.Join and returned as a single error. You can inspect individual errors with errors.Is and errors.As:

err := c.ServiceStartup(ctx, nil)
if err != nil {
    // err may contain multiple wrapped errors
    if errors.Is(err, context.Canceled) {
        // context was cancelled
    }
}

Context Cancellation

Both ServiceStartup and ServiceShutdown respect context cancellation. If the context is cancelled or its deadline is exceeded, the remaining services are skipped:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

err := c.ServiceStartup(ctx, nil)
// If startup takes longer than 5 seconds, remaining services are skipped

Detection

Lifecycle interface detection happens at registration time. When you call RegisterService, Core checks whether the service implements Startable and/or Stoppable and adds it to the appropriate internal list. There is no need to declare anything beyond implementing the interface.