goscade
Minimalistic lifecycle orchestrator for Go components with dependency graphs, startup sequencing, readiness coordination, and graceful shutdown
About goscade
GOscade
GOscade is a lightweight lifecycle orchestrator for Go applications.
It coordinates startup, readiness, failure propagation, and graceful shutdown of dependent components.
- Dependency-aware startup
- Readiness coordination
- Failure propagation
- Graceful shutdown in reverse dependency order
- Automatic dependency graph discovery
Installation
go get github.com/ognick/goscade/v2
Component Interface
To be managed by GOscade, a type must implement the Component interface:
type Component interface {
// Run starts the component.
// It must call readinessProbe when the component is ready to serve.
// It should block until ctx is canceled or a fatal error occurs.
Run(ctx context.Context, readinessProbe func(cause error)) error
}
Usage
Basic Example
package main
import (
"context"
"log"
"time"
"github.com/ognick/goscade/v2"
)
type Worker struct {
Name string
}
func (w *Worker) Run(ctx context.Context, readinessProbe func(error)) error {
log.Printf("Worker %s starting...", w.Name)
// Simulate startup initialization
time.Sleep(100 * time.Millisecond)
readinessProbe(nil) // Signal readiness
log.Printf("Worker %s ready", w.Name)
<-ctx.Done() // Wait for shutdown signal
log.Printf("Worker %s stopped", w.Name)
return nil
}
// Simple logger adapter
type myLogger struct{}
func (l myLogger) Infof(f string, a ...interface{}) { log.Printf(f, a...) }
func (l myLogger) Errorf(f string, a ...interface{}) { log.Printf("ERROR: "+f, a...) }
func main() {
// 1. Create Lifecycle manager
lc := goscade.NewLifecycle(myLogger{}, goscade.WithShutdownHook())
// 2. Register components
goscade.Register(lc, &Worker{Name: "Core"})
// 3. Run
// Blocks until SIGINT/SIGTERM or error
err := goscade.Run(context.Background(), lc, func() {
log.Println("System is fully up and running!")
})
if err != nil {
log.Fatal(err)
}
}
Dependency Discovery
GOscade automatically detects dependencies between registered components if they are struct pointers or interfaces.
type Database struct { /* ... */ }
// Service depends on Database via interface
type Service struct {
DB interface{} // or specific interface like Storer
}
func main() {
lc := goscade.NewLifecycle(log)
db := &Database{}
service := &Service{DB: db}
// Order of registration doesn't matter
goscade.Register(lc, service)
goscade.Register(lc, db)
// GOscade detects that Service depends on Database.
// It ensures Database is ready before Service starts.
}
To exclude a struct field from reflection-based dependency detection, tag it with goscade:"ignore". The field (and anything reachable through it) will not be traversed.
This is useful for externally-managed dependencies that GOscade does not own. Such third-party clients may spawn their own goroutines, so letting GOscade treat them as part of the managed lifecycle can lead to race conditions. Tagging them as ignored keeps them out of the dependency graph:
type Service struct {
DB *Database
PaymentAPI *stripe.Client `goscade:"ignore"` // externally-managed, may run its own goroutines
}
If you need to declare dependencies that cannot be detected via reflection (e.g. hidden inside closures or non-struct fields), use explicit declaration:
// Register service and declare it depends on db explicitly
goscade.Register(lc, service, db)
Linking dependencies through an arbitrary struct
When a component has no reflectable field path to the components it depends on
(the link runs through a wiring struct, closures, etc.), use Link to point it
at an arbitrary struct. The struct is reflection-walked during graph building
and every registered component reachable inside it becomes a parent — as if the
component had a field referencing that struct. The struct itself is never a node
and is never run.
// wiring is a plain struct, not a Component.
type wiring struct {
DB *Database
Cache *Cache
}
w := &wiring{DB: db, Cache: cache}
// service depends on every registered component reachable inside w (db, cache),
// even though service has no field referencing them.
goscade.Link(lc, service, w)
Adapter Pattern
Use NewAdapter to wrap existing types (like http.Server) without defining a new struct.
srv := &http.Server{Addr: ":8080"}
adapter := goscade.NewAdapter(srv, func(ctx context.Context, s *http.Server, probe func(error)) error {
// Start server in background
errChan := make(chan error, 1)
go func() {
if err := s.ListenAndServe(); err != http.ErrServerClosed {
errChan <- err
}
}()
// Signal ready (optionally verify port is listening first)
probe(nil)
// Wait for context cancellation or server error
select {
case <-ctx.Done():
// Graceful shutdown
return s.Shutdown(context.Background())
case err := <-errChan:
return err
}
})
lc.Register(adapter)
Configuration Options
lc := goscade.NewLifecycle(logger,
// Handle system signals (SIGINT, SIGTERM)
goscade.WithShutdownHook(),
// Set timeout for components to become ready
goscade.WithStartTimeout(30 * time.Second),
// Set timeout for components to stop
goscade.WithShutdownTimeout(30 * time.Second),
// Allow circular dependencies (use with caution)
goscade.WithCircularDependency(),
// Export dependency graph to DOT file on startup
goscade.WithGraphOutput("graph.dot"),
)
Errors
Lifecycle.Run returns the cause that initiated shutdown together with any
independent component and cleanup errors. Inspect the result with errors.Is;
multiple failures may be joined with errors.Join.
err := lc.Run(ctx, nil)
if errors.Is(err, context.Canceled) {
// Shutdown was requested by the caller.
}
Dependency Graph Export
GOscade can export the component dependency graph in DOT format (Graphviz).
Get Graph Programmatically
// Build graph structure
graph := lc.BuildGraph()
// Convert to DOT format
dotString := graph.ToDOT()
fmt.Println(dotString)
Auto-save to File
// Graph will be saved to file when lifecycle starts
lc := goscade.NewLifecycle(logger,
goscade.WithGraphOutput("graph.dot"),
)
Visualize with Graphviz
# Generate PNG image
dot -Tpng graph.dot -o graph.png
# Generate SVG
dot -Tsvg graph.dot -o graph.svg
# Generate PDF
dot -Tpdf graph.dot -o graph.pdf
Example DOT output:
digraph G {
rankdir=TB;
"Database" [label="Database", shape=box];
"Cache" [label="Cache", shape=box];
"APIServer" [label="APIServer", shape=box];
"Database" -> "Cache";
"Cache" -> "APIServer";
}
Visual Examples
License
This project is licensed under the MIT License - see the LICENSE file for details.
Frequently Asked Questions
What is goscade?
goscade is a Goroutines library for the Go programming language. Minimalistic lifecycle orchestrator for Go components with dependency graphs, startup sequencing, readiness coordination, and graceful shutdown
How do I install goscade?
Install goscade with the Go module system using `go get ognick/goscade`. Check the repository for the current installation instructions.
What category does goscade belong to?
goscade is listed under Goroutines, specifically Goroutines.