Architecture
Servicepack is a clone-and-own application skeleton. It gives a project a well-defined process lifecycle and a place for multiple services; it does not require every future deployment to stay inside one binary.
Runtime shape
cmd/main.go
├─ set global log scope: binary, commit, version
├─ services.Init() generated factory registration
└─ Cobra "run"
└─ pkg/runner
├─ signal / parent-context handling
└─ internal/app.App
├─ pre-run hooks
├─ internal/pkg/service-manager
│ └─ service factories → services running concurrently
└─ post-stop hooks
cmd/main.go is intentionally small: establish process identity, register generated service factories, expose run and command namespaces, then pass control to the runner. The runner README explains the signal and deadline rules.
internal/app owns application-level hooks and delegates service execution to the service manager. App is the place for whole-process behavior; individual services should not reach across into siblings to create their own lifecycle graph.
Local composition and deployment choices
During local development, keeping related services in one process makes debugging concrete: one binary, one cancellation path, one structured log stream, and direct visibility into failures. This is the default development shape:
my-service process
├─ api service
├─ worker service
├─ scheduler service
└─ migration command namespace
Production has two valid shapes:
one release unit independently deployed units
---------------- ----------------------------
my-service binary api binary ──────┐
├─ api worker binary ───┼─ explicit HTTP/gRPC/queue contracts
├─ worker scheduler binary ┘
└─ scheduler
Choose one binary when the services have the same release cadence and operational boundary. Split when they need separate scaling, ownership, security boundaries, or failure isolation. Splitting is an architecture change: replace in-process calls and service-manager dependency declarations with explicit APIs, messages, authentication, retries, observability, and deploy configuration.
SERVICES_ENABLED is useful for a partial local run within one binary; it is not a microservice deployment system.
Source ownership
| Path | Role | Ownership after make own |
|---|---|---|
cmd/main.go | Process entry point and root CLI. | Framework |
cmd/init.go | Extra handlers and application hooks. | Project |
cmd/commands.go | App-level CLI commands. | Project |
internal/app/ | App lifecycle wrapper. | Framework |
internal/pkg/service-manager/ | Concurrency, dependency, retry, and stop semantics. | Framework |
internal/pkg/services/ | Business services and services.gen.go. | Project; generated registration is not hand-edited. |
pkg/runner/ | Signal-aware runner. | Framework |
scripts/make/servicepack/ | Updateable Make implementations. | Framework |
scripts/make/ | Project-specific target overrides. | Project |
Makefile.servicepack | Framework Make target definitions. | Framework |
Makefile | Project targets and overrides. | Project |
Framework-owned means an update may replace it. Project-owned means the update’s normal exclusion policy preserves it. See framework updates before changing that boundary.
Registration and lazy construction
make service-registration runs the generator that discovers Service implementations and writes internal/pkg/services/services.gen.go. The generated code registers factories rather than fully constructed services.
That distinction matters:
runinstantiates all enabled factories;- a per-service Cobra command instantiates only that one service;
- connections and config parsing happen in a service’s
New, not at package import time.
This keeps command execution from accidentally opening every database/client in the project. Details are in the service-manager README.
Observability and configuration
Logging starts with the slogging handler setup and flows through ctxscope. The binary, build commit and version are global scope; the service manager adds a service field while it runs or stops a service. Preserve and extend the context passed into Run; it carries cancellation and those fields together.
Configuration is typed and parsed at each service boundary with gonfiguration. Framework settings are documented in getting started; application settings belong in the project’s own docs and config examples.
Build and test topology
The Makefile runs tooling in Docker. Test targets additionally receive Docker access for Testcontainers. The development model, coverage boundary, and override mechanism are documented in development, with the exact script behavior in the Make-script README.