Skip to content

A Go framework for building concurrent service applications without the usual boilerplate bullshit. Write once, deploy everywhere - run your entire stack locally for debugging or distribute services across machines with a single env var. No Docker Compose nightmares or managing 47 separate repos.

License

Notifications You must be signed in to change notification settings

psyb0t/servicepack

Repository files navigation

___ ___ _____   _____ ___ ___ ___  _   ___ _  __
/ __| __| _ \ \ / /_ _/ __| __| _ \/_\ / __| |/ /
\__ \ _||   /\ V / | | (__| _||  _/ _ \ (__| ' <
|___/___|_|_\ \_/ |___\___|___|_|/_/ \_\___|_|\_\

servicepack

A Go service framework that runs your shit concurrently without fucking around.

Table of Contents

Getting Started

Core Concepts

Essential Tools

System Details

Framework Management

Advanced Topics

Reference

What is this?

You write services, this thing runs them. All your services go into one binary so you can debug the fuck out of service-to-service calls without dealing with distributed bullshit. Run everything locally, then deploy individual services as microservices when you're ready. Or just fuckin' deploy everything together, y not.

Quick Start (Make It Your Own in 30 Seconds)

# Clone this shit
git clone https://github.com/psyb0t/servicepack
cd servicepack

# Make it yours
make own MODNAME=github.com/yourname/yourproject

# Build and run
make build
./build/yourproject run

This will:

  • Nuke the .git directory
  • Replace the module name everywhere
  • Set you up with a fresh go.mod
  • Replace README with just your project name
  • Run git init to start fresh
  • Setup dependencies
  • Create initial commit on main branch

You'll see the hello-world service spamming "Hello, World!" every 5 seconds. Hit Ctrl+C to stop it cleanly.

Just Want to Try It First?

git clone https://github.com/psyb0t/servicepack
cd servicepack
make build
./build/servicepack run

Creating Services

Create a new service:

make service NAME=my-cool-service

This shits out a skeleton service at internal/pkg/services/my-cool-service/. Edit the generated file, put your logic in the Run() method. Done - your service starts automatically.

Remove a service:

make service-remove NAME=my-cool-service

Service Interface

Every service implements this interface:

type Service interface {
    Name() string                        // Return service name
    Run(ctx context.Context) error      // Your service logic goes here
    Stop(ctx context.Context) error     // Cleanup logic (optional)
}

The Run() method should:

  • Listen for ctx.Done() and return cleanly when cancelled
  • Return an error if something goes wrong (this will stop all services)
  • Do whatever the fuck your service is supposed to do

The Stop() method is for cleanup - it runs when the app is shutting down.

How Services Actually Work

  1. Services are auto-discovered using the gofindimpl tool
  2. The scripts/make/service_registration.sh script finds all Service implementations
  3. It generates internal/pkg/services/services.gen.go with a services.Init() function
  4. The services.Init() function is called when the app starts to register all services
  5. Services get filtered based on the SERVICES_ENABLED environment variable

Service Filtering

By default, all services run. To run specific services:

export SERVICES_ENABLED="hello-world,my-cool-service"
./build/servicepack run

Leave SERVICES_ENABLED empty or unset to run all services.

The Makefile (Your New Best Friend)

Basic Commands

  • make all - Full pipeline: dep → lint-fix → test-coverage → build
  • make build - Build the binary using Docker (static linking)
  • make dep - Get dependencies with go mod tidy and go mod vendor
  • make test - Run all tests with race detection
  • make test-coverage - Run tests with 90% coverage requirement (excludes hello-world and cmd packages)
  • make lint - Lint your code with comprehensive golangci-lint rules (80+ linters enabled)
  • make lint-fix - Lint and auto-fix issues
  • make clean - Clean build artifacts and coverage files

Service Management

  • make service NAME=foo - Create new service
  • make service-remove NAME=foo - Remove service
  • make service-registration - Regenerate service discovery

Development

  • make run-dev - Run in development Docker container
  • make docker-build-dev - Build dev image

Docker

  • make docker-build - Build production Docker image
  • make docker-build-dev - Build development Docker image

Framework Management

  • make servicepack-update - Update to latest servicepack framework (creates backup first)
  • make servicepack-update-review - Review pending framework update changes
  • make servicepack-update-merge - Merge pending framework update
  • make servicepack-update-revert - Revert pending framework update
  • make own MODNAME=github.com/you/project - Make this framework your own

Backup Management

  • make backup - Create timestamped backup in /tmp and .backup/
  • make backup-restore [BACKUP=filename.tar.gz] - Restore from backup (defaults to latest, nukes everything first)
  • make backup-clear - Delete all backup files

Note: Framework updates (make servicepack-update) automatically create backups before making changes.

Script Customization

You can override any framework script by creating a user version:

# Create custom script (will override framework version)
cp scripts/make/servicepack/test.sh scripts/make/test.sh
# Edit your custom version
vim scripts/make/test.sh

The Makefile checks for user scripts first (scripts/make/), then falls back to framework scripts (scripts/make/servicepack/). This lets you customize any build step while preserving the ability to update the framework without conflicts.

Framework scripts (in scripts/make/servicepack/):

  • Get updated when you run make servicepack-update
  • Always preserved - your customizations won't get overwritten

User scripts (in scripts/make/):

  • Take priority over framework scripts
  • Never touched by framework updates
  • Perfect for project-specific build customizations

Makefile Customization

The build system uses a split Makefile approach:

# Override any framework command by defining it in your Makefile
build: ## Custom build command
	@echo "Running my custom build..."
	@docker build -t myapp .

# Add your own custom commands
deploy: ## Deploy to production
	@./deploy.sh

How it works:

  • Makefile.servicepack - Contains all framework commands (updated by framework)
  • Makefile - Your file that includes servicepack + allows custom commands (never touched)
  • User commands override framework commands automatically
  • make help shows both user and framework commands

Dockerfile Customization

Both development and production Docker environments use the override pattern:

# Customize development environment
cp Dockerfile.servicepack.dev Dockerfile.dev
vim Dockerfile.dev

# Customize production environment
cp Dockerfile.servicepack Dockerfile
vim Dockerfile

How it works:

  • Dockerfile.servicepack.dev - Framework development image (updated by framework)
  • Dockerfile.dev - Your custom development image (never touched)
  • Dockerfile.servicepack - Framework production image (updated by framework)
  • Dockerfile - Your custom production image (never touched)
  • make docker-build-dev automatically uses your custom development version if it exists

Architecture

cmd/main.go                          # Entry point, CLI setup
internal/app/                        # Application layer
├── app.go                          # Main app orchestration
├── config.go                       # Configuration parsing
internal/pkg/
├── service-manager/                 # Framework service orchestration
│   ├── service_manager.go          # Concurrent service runner
│   ├── errors.go                   # Framework error definitions
│   └── *_test.go                   # Framework tests
└── services/                       # User service space
    ├── services.gen.go             # Auto-generated services.Init() function
    ├── hello-world/                # Example service
    ├── my-cool-service/            # Your service (one dir per service)
    └── another-service/            # Another service
scripts/make/                        # Build script system
├── servicepack/                    # Framework scripts (updated by framework)
│   ├── build.sh                   # Docker build script
│   ├── dep.sh                     # Dependency management
│   ├── test.sh                    # Test runner
│   └── *.sh                       # Other framework scripts
└── [custom scripts]               # User overrides (take priority)

Key Components

ServiceManager: Runs your services concurrently, handles shutdown, routes errors. It's a singleton because globals are fine when you know what you're doing.

Service Registration: Auto-discovery using gofindimpl finds all your Service implementations and generates a services.Init() function. No manual registration bullshit.

App: Wrapper that runs the ServiceManager and handles the lifecycle shit.

Environment Variables

The framework uses these:

# Logging (via logrus-configurator)
LOG_LEVEL=debug          # trace, debug, info, warn, error
LOG_FORMAT=json          # json, text
LOG_CALLER=true          # show file:line in logs

# Service filtering
SERVICES_ENABLED=service1,service2   # comma-separated, empty = all

# Your services can define their own env vars

Build System Details

The build system is dynamic as fuck:

  1. App name is extracted from go.mod automatically
  2. Binary gets built with static linking (no external deps)
  3. App name is injected at build time via ldflags
  4. Docker builds ensure consistent environment

Build Process

APP_NAME := $(shell head -n 1 go.mod | awk '{print $2}' | awk -F'/' '{print $NF}')

build:
    docker run --rm -v $(PWD):/app -w /app golang:1.24.6-alpine \
        sh -c "apk add --no-cache gcc musl-dev && \
               CGO_ENABLED=0 go build -a \
               -ldflags '-extldflags \"-static\" -X main.appName=$(APP_NAME)' \
               -o ./build/$(APP_NAME) ./cmd/..."

This means your binary name matches your module name automatically.

Framework Updates

Keep your servicepack framework up to date:

make servicepack-update

This script:

  1. Checks for uncommitted changes (fails if found)
  2. Compares current version with latest
  3. Creates backup if update is needed
  4. Creates update branch servicepack_update_to_VERSION
  5. Downloads latest framework and applies changes
  6. Commits changes to update branch for review
  7. Leaves you on update branch to review and test

Review and Apply Updates

After running make servicepack-update:

# Review what changed
make servicepack-update-review

# Test the update
make dep && make service-registration && make test

# If satisfied, merge the update
make servicepack-update-merge

# If not satisfied, discard the update
make servicepack-update-revert

Customizing Updates with .servicepackupdateignore

Create a .servicepackupdateignore file to exclude files from framework updates:

# Custom framework modifications (these are already user files)
# Note: Dockerfile, Dockerfile.dev, Makefile, and scripts/make/ are automatically excluded

# Local configuration files
*.local
.env*

Framework vs User Files:

cmd/                           # Framework files
internal/app/                  # Framework files
internal/pkg/service-manager/  # Framework files
scripts/make/servicepack/      # Framework scripts (updated by servicepack-update)
scripts/make/                  # User scripts (override framework, never touched)
Makefile.servicepack           # Framework Makefile (updated by servicepack-update)
Makefile                       # User Makefile (includes servicepack, never touched)
Dockerfile.servicepack.dev     # Framework development image (updated by servicepack-update)
Dockerfile.dev                 # User development image (overrides framework, never touched)
Dockerfile.servicepack         # Framework production image (updated by servicepack-update)
Dockerfile                     # User production image (never touched)
.github/                       # Framework files (CI/CD workflows)
LICENSE                        # Your project license
.golangci.yml                  # Framework files
go.mod                         # Your module name preserved
go.sum                         # Gets regenerated
README.md                      # Your project docs
internal/pkg/services/         # Your services - never touched

Use .servicepackupdateignore to exclude any framework files you've customized.

Pre-commit Hook

There's a pre-commit.sh script that runs make lint && make test-coverage. You can:

  • Use your favorite pre-commit tool to manage hooks
  • Use ez-pre-commit to auto-setup Git hooks that run this script
  • Just use the simple script as-is (it runs lint and coverage checks)

Testing

Tests are structured per component:

  • internal/app/app_test.go - Application tests with mock services
  • internal/pkg/services/service_manager_test.go - Service manager tests with concurrency testing
  • internal/pkg/services/errors_test.go - Error definition and matching tests
  • Each service should have its own *_test.go files

90% test coverage is required by default (excludes hello-world service). The coverage check runs with race detection and fails if below threshold.

Test Isolation

  • ResetInstance() resets the singleton for clean test state
  • ClearServices() clears all registered services
  • Mock services implement the Service interface for testing
  • Tests should avoid calling services.Init() and manually add mock services instead

Concurrency Model

  • Each service runs in its own goroutine
  • ServiceManager uses sync.WaitGroup for coordination
  • Context cancellation for clean shutdown
  • Services can fail independently (one failure stops all)
  • Graceful shutdown with configurable timeout

Error Handling

  • Service errors bubble up through the ServiceManager
  • First error stops all services
  • Context errors (cancellation) are treated as clean shutdown
  • All errors use ctxerrors for context preservation
  • ErrNoEnabledServices is returned when no services are registered (empty service list)

Dependencies

Core dependencies:

Development dependencies:

  • golangci-lint - Comprehensive linting (80+ linters: errcheck, govet, staticcheck, gosec, etc.)
  • testify - Testing assertions and mocks
  • gofindimpl - Service auto-discovery tool

Directory Structure

.
├── cmd/main.go                     # Entry point
├── internal/
│   ├── app/                        # Application layer
│   └── pkg/services/               # Services
├── scripts/make/                   # Build script system
│   ├── servicepack/               # Framework scripts (auto-updated)
│   └── [user scripts]             # User overrides (take priority)
├── build/                          # Build output
├── vendor/                         # Vendored dependencies
├── Makefile                        # User Makefile (includes servicepack framework)
├── Makefile.servicepack            # Framework Makefile (auto-updated)
├── Dockerfile                      # User production image (optional override)
├── Dockerfile.dev                  # User development image (optional override)
├── Dockerfile.servicepack          # Framework production image (auto-updated)
├── Dockerfile.servicepack.dev      # Framework development image (auto-updated)
└── servicepack.version             # Framework version tracking

Future Features (TODO)

  • Service Retry: When a service shits itself, check retry count and restart the fucker if it hasn't hit the limit yet
  • Allowed Failures: Let some services die without killing everything - useful for one-shot jobs like migrators that run once and fuck off
  • Service Dependencies: Let services say "I need this other shit to start first" so database comes up before API and shit
  • Health Checks: Built-in endpoints to check if services are alive or dead with timeouts and failure limits
  • Management API: HTTP endpoint to see what's running and control the bastards (start/stop/restart individual services)
  • Metrics: Track startup times, failure counts, restart counts and optionally export to Prometheus
  • Service Communication: Built-in message passing so services can talk to each other instead of figuring that shit out themselves

License

MIT

About

A Go framework for building concurrent service applications without the usual boilerplate bullshit. Write once, deploy everywhere - run your entire stack locally for debugging or distribute services across machines with a single env var. No Docker Compose nightmares or managing 47 separate repos.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published