Skip to content

Building a Fountain Service

A Fountain service is a runnable microservice scaffolded and validated against a fixed set of conventions. Those conventions are not just style preferences — they are machine-checked by ftkit verify, which walks the repository and reports every deviation as a warning or error. Building "to the Fountain standard" means structuring your code so that this verification passes.

What "Fountain standard" means

A standard-compliant service follows a layered architecture with a fixed directory structure. ftkit verify enforces it: it checks that the required directories exist, that main.go and server/server.go follow the expected bootstrap and lifecycle patterns, that controllers register themselves correctly, that handlers and DAOs obey their placement and naming rules, and that the codebase uses flog for logging. A repo that fails verification is, by definition, not a Fountain-standard service.

The required directories are server, biz, biz/core, and biz/dal. Everything else (biz/dal/dao, biz/dal/models, biz/dal/do, pkg, cmd, docs, scripts, …) is optional and verified only when present.

The layers

A Fountain service is organized into layers, each with a single responsibility, wired together from the entry point down to storage:

text
main.go  →  server/  →  biz/core/  →  biz/dal/  →  pkg/  →  <service>_client/
            (servers)   (controllers)  (dao,        (shared   (generated
                                         models, do)  packages)  client lib)
  • main.go — the entry point. It bootstraps the framework with fountain.WithAppInstances(...) (or fountain.New()), constructs the server via server.NewServer(), and starts it with .Serving().
  • server/ — the transport and lifecycle layer. It defines the server struct, its NewServer() constructor, the lifecycle methods (Initialize, Serving, Destroy, Info), route wiring (server/route.go), and the HTTP/gRPC handlers that translate requests into controller calls. It blank-imports _ ".../biz" so the business layer registers itself.
  • biz/core/ — the controllers, one package per business domain. Each controller implements core.CoreController (InstallController(), RegisterCallback(...)), registers itself with RegisterCoreController(...), and holds the business logic for its domain. This is the only layer that should contain business rules.
  • biz/dal/ — the data access layer. It splits into dao/ (data access objects and their managers.go registry), models/ (persistence models for databases), and do/ (data objects used by the cache/Redis layer). DAOs are the only code that talks to a database, cache, or storage backend directly.
  • pkg/ — shared, reusable packages internal to the service that do not belong to any single layer (helpers, shared types, integrations).
  • <service>_client/ — a client library other services import to call this service. It exposes a typed client (client.go) so callers never hand-build requests.

Lifecycle & install order

The server struct implements the four lifecycle methods that the framework drives:

  • Initialize() — wires up every dependency the service needs (blocking, one-time).
  • Serving() — runs the server; this is the blocking call that keeps the process up.
  • Destroy() — releases resources on shutdown.
  • Info() — returns metadata describing the server instance.

Inside Initialize(), dependencies must be installed in a specific order so that each step's prerequisites already exist. ftkit verify checks this order (only InstallCoreControllers — the controllers step — is strictly required; the rest are verified when present):

  1. loggerInstall logger
  2. cachesInstall caches
  3. databasesInstall databases
  4. daoInstall dao
  5. clientsInstall clients
  6. serversInstall servers
  7. controllersInstallCoreControllers (required)
  8. handlersInstall handlers

The logic is bottom-up: the logger comes first so everything can log; caches and databases come next; DAOs depend on those backends; clients and servers depend on DAOs; controllers are installed once their DAOs and clients exist; and handlers are wired last because they depend on the controllers they invoke.