Core Concepts
This guide explains the core building blocks of Fountain: the application singleton, the runnable lifecycle and its ordering, the runnable kinds you register, core controllers and their observer pattern, the two library integration patterns, and the conventions the framework follows. Understanding these concepts is the foundation for everything else in the framework.
The Fountain singleton
Fountain is bootstrapped through a singleton Fountain struct. You obtain it with New(), register the components you want to run using fluent builder methods, and start the lifecycle with Serving(). New() returns the same instance on every call (it is constructed once via sync.Once), so the configuration you accumulate through the builder methods all applies to one shared application.
The builder methods register the four runnable kinds and return *Fountain so calls can be chained:
WithAppInstances(appInstances ...runnable.AppInstance)— long-running servers.WithInvokers(invokers ...runnable.Invoker)— one-shot initialization functions.WithJobs(jobs ...runnable.JobInstance)— one-shot jobs.WithCrons(crons ...runnable.JobInstance)— scheduled jobs.
Serving() is the blocking entry point that runs the registered components in order and keeps the process alive until a shutdown signal arrives.
fountain.New().
WithAppInstances(httpServer, grpcServer).
WithInvokers(initDB).
WithCrons(dailyReport).
Serving()Lifecycle & ordering
When you call Serving(), Fountain runs the registered kinds in a fixed order:
Invokers → Jobs → Crons → AppInstances → Auxiliary
- Invokers run first. They execute concurrently when the
concurrencyflag is on, otherwise sequentially. - Jobs run next, following the same concurrency rule as invokers.
- Crons run after that. When crons are present, both the
concurrencyandhangflags are automatically enabled so they run in parallel and keep the application alive. - AppInstances are then initialized and served.
concurrencyandhangare enabled here as well so the servers can run in parallel and keep the process running continuously. - Auxiliary server (metrics / governance) starts when
hangis enabled, keeping the application running until a shutdown signal (shutdownSignals, e.g.SIGTERM/SIGINT) is received.
Two flags govern this behavior:
concurrency— affects invokers and jobs. When on, those components are launched in goroutines (coordinated with async.WaitGroup) and run in parallel. When off, they run sequentially: each component must finish before the next starts.hang— keeps the application in a waiting state rather than exiting immediately after work completes. It is typically paired withconcurrencyso the process waits for a system shutdown signal. Whenhangis off,Serving()does not wait and exits as soon as the registered work finishes — suitable for short, fast tasks.
On shutdown, Fountain runs beforeStopClean functions, stops jobs and crons, destroys instances, then runs afterStopClean functions.
Runnable kinds
The components you register implement interfaces defined in the runnable package.
AppInstance
An AppInstance is a long-running server (an HTTP, gRPC, TCP, UDP, or QUIC server). Fountain initializes it, runs Serving() in a goroutine, and destroys it on shutdown. The interface is:
// AppInstance type;
type AppInstance interface {
Initialize() error
// Serving was called in a goroutine
Serving()
Destroy() error
OnConfigChange()
Info() ServerInstance
}Initialize() prepares the instance, Serving() runs the server (invoked in a goroutine, so it may block), Destroy() releases resources on shutdown, OnConfigChange() is the callback fired when configuration changes, and Info() returns the ServerInstance metadata (name, ID, protocol, address, and so on) used for things like service discovery.
Invoker
An Invoker is a one-shot initialization function — the simplest runnable kind. It is just a function that returns an error:
type Invoker func() errorInvokers run first in the lifecycle, making them the right place for setup work such as connecting to a database or warming a cache.
JobInstance
A JobInstance is a one-shot or scheduled unit of work. Both WithJobs(...) (one-shot jobs) and WithCrons(...) (scheduled jobs) accept JobInstance values. The interface is:
// JobInstance type;
type JobInstance interface {
Start() error
GetName() string
Stop() error
}Start() runs the job, GetName() identifies it, and Stop() is called during shutdown.
Core controllers & the observer pattern
Business logic in Fountain is organized into core controllers. A controller implements the CoreController interface from the core package:
// CoreController interface
type CoreController interface {
InstallController()
RegisterCallback(cb any)
}You register controllers with core.RegisterCoreController(&MyController{}), then wire them up with core.InstallCoreControllers() once their dependencies (databases, caches, and so on) are available. During installation, Fountain calls InstallController() on each controller and then calls RegisterCallback(...) on each controller for every other controller, allowing controllers to discover one another.
core.RegisterCoreController(&UserController{})
core.RegisterCoreController(&OrderController{})
// after databases, caches, etc. are installed:
core.InstallCoreControllers()Controllers communicate through an observer pattern. A controller that wants to receive events implements the generic CoreControllerObserver[T] interface (an Update(event EventType, params ...T) method). To broadcast an event to all controllers that observe that type, call core.NotifyObservers(...):
core.NotifyObservers[*User](EventUserCreated, user)NotifyObservers iterates over the registered controllers and invokes Update(...) on any controller that implements the matching observer interface, keeping controllers decoupled while still able to react to one another's events.
Integration patterns
Infrastructure libraries (servers, databases, caches, brokers, clients) are installed into the Fountain instance through one of two patterns.
Config-key pattern — bind the component to a nested key in config.yaml and install it:
client := fedis.WithConfigKey("redis").InstallFountainInstance()Functional-options pattern — configure the component inline with With* option functions:
server := fgrpc.WithConfigKey("fgrpc").
InstallFountainInstance(fgrpc.WithServerOptions(serverOptions...))The two patterns compose: a common form is to select the config key with WithConfigKey(...) and then pass functional options to InstallFountainInstance(...). Configuration lives in config.yaml with nested keys, and environment variables use the FOUNTAIN_ prefix.
Conventions
Fountain follows a consistent set of conventions across the codebase:
- Package naming — utility/library packages use an
f*prefix:flog,fedis,fhttp,fgrpc, and so on. Protocol client directories usef<protocol>_client/. - Constants — global constants use a
Kprefix inUPPER_SNAKE_CASE(for exampleKPackageName,KServerKindProvider). - Import ordering — group imports as: standard library → third-party →
gitlab.soludian.com/soludian/fountain/...→ local packages. - Logging — use
flog, not the standard librarylogpackage. Obtain a logger withflog.NewFountainLoggerOnce(). - Errors — use the helpers in
libs/ferr/for error wrapping and handling.