Service Disposal & Cleanup in Parsley

In 
Learn how to manage resource cleanup and lifecycle disposal for singleton and scoped services using the Disposable interface in Parsley.

When building robust Go applications, managing the lifecycle of resources such as database connections, file handles, or network clients is critical. Parsley provides built-in support for explicit resource cleanup and automatic service disposal through the Disposable interface and resolver shutdown methods.

The Disposable Interface

Any service that requires explicit cleanup when it is no longer needed can implement the types.Disposable interface:

type Disposable interface {
    Dispose(ctx context.Context) error
}

When services managed by Parsley implement Dispose(ctx context.Context) error, the framework can automatically track them and invoke their cleanup routines during application shutdown or scope disposal.

Singleton Service Disposal: Resolver.Shutdown

Singleton services remain valid for the lifetime of the resolver. To clean up all registered singleton services that implement Disposable, you can call the Shutdown method on the Resolver interface:

err := resolver.Shutdown(ctx)

Example

Consider a disposable service registered as a singleton:

type DatabaseClient struct {
    closed bool
}

func (db *DatabaseClient) Dispose(ctx context.Context) error {
    db.closed = true
    return nil
}

func NewDatabaseClient() *DatabaseClient {
    return &DatabaseClient{}
}

Registering and shutting down the singleton service:

registry := registration.NewServiceRegistry()
_ = registration.RegisterSingleton(registry, NewDatabaseClient)

resolver := resolving.NewResolver(registry)
ctx := resolving.NewScopedContext(context.Background())

// Resolve the singleton service
client, _ := resolving.ResolveRequiredService[*DatabaseClient](ctx, resolver)

// Shut down the resolver to clean up singletons
err := resolver.Shutdown(ctx)
if err != nil {
    // Handle error (or NewAggregateError if multiple services return errors)
}

Scoped Service Disposal: DisposeScope

For services registered with a scoped lifetime, instances are tied to a specific context (such as an HTTP request or task scope). You can dispose of all scoped services associated with a context by calling DisposeScope:

err := resolving.DisposeScope(ctx)

Example

registry := registration.NewServiceRegistry()
_ = registration.RegisterScoped(registry, NewDatabaseClient)

resolver := resolving.NewResolver(registry)
ctx := resolving.NewScopedContext(context.Background())

// Resolve scoped service
client, _ := resolving.ResolveRequiredService[*DatabaseClient](ctx, resolver)

// Dispose the scope when the request or task completes
err := resolving.DisposeScope(ctx)

Note: Transient services are not tracked by the resolver or scope bags and are not automatically disposed by Shutdown or DisposeScope. Callers are responsible for cleaning up transient instances if needed.

Disposal Order and Thread Safety

  • Reverse Order of Creation: When disposing singleton or scoped instances, Parsley automatically tracks instances in an internal container (InstanceBag) and disposes them in the reverse order of their creation. This ensures that dependencies created earlier outlive their dependents during teardown.
  • Thread Safety: Service disposal is thread-safe, protecting against concurrent shutdown or scope disposal requests.
  • Error Aggregation: If multiple services return an error during Dispose, Parsley aggregates them using ParsleyError in pkg/types.

Automatic Application Shutdown

When bootstrapping applications using bootstrap.RunParsleyApplication, Parsley automatically handles the disposal of singleton and scoped services upon application exit, ensuring clean resource teardown without manual Shutdown calls.