Plumego Advanced Guide: Designing Explicit, Scalable Go Services
Birdor Engineering
Plumego Advanced Guide: Designing Explicit, Scalable Go Services
Introduction: Beyond the Basics
The introductory Plumego documentation explains what Plumego is and why it exists. This guide focuses on a different question:
How do you design serious, long-lived systems with Plumego?
This article assumes that you already understand:
- Go fundamentals
- HTTP server basics
- The core Plumego APIs
- The philosophy of explicitness
What follows is an advanced, production-oriented tutorial. We will explore architectural patterns, not just APIs. The emphasis is on design decisions, trade-offs, and operational correctness—the areas where Plumego provides leverage without hiding complexity.
Mental Model: Plumego as a Runtime Skeleton
Before diving into code, it is important to internalize Plumego’s role in your system.
Plumego is not:
- A full-stack framework
- A domain framework
- A business logic container
Plumego is:
- A runtime skeleton
- A transport and lifecycle coordinator
- A boundary-definition tool
Think of Plumego as the structural steel of a building. It defines how components connect, but it does not dictate how rooms are furnished.
Project Layout for Advanced Systems
Plumego does not enforce a directory structure, but experienced teams converge on similar patterns.
A Recommended High-Level Layout
cmd/
server/
main.go
internal/
app/
server.go
lifecycle.go
transport/
http/
router.go
middleware.go
handlers/
websocket/
domain/
user/
entity.go
service.go
repository.go
usecase/
user/
create_user.go
get_user.go
infra/
db/
cache/
metrics/
pkg/
observability/
auth/
Why This Layout Works with Plumego
cmd/owns process startupinternal/appwires dependencies explicitlytransportis isolated from domain logicdomaincontains no framework importsusecaseorchestrates behaviorinfraowns side effects
Plumego naturally supports this layout because it does not attempt to collapse layers into “controllers” or “services.”
Server Construction as an Architectural Boundary
In advanced Plumego usage, server construction is a critical architectural boundary.
Avoid “Fat main.go”
Instead of registering everything in main.go, delegate responsibilities:
func main() {
cfg := LoadConfig()
app := NewApplication(cfg)
if err := app.Run(); err != nil {
log.Fatal(err)
}
}
Application as a Composition Root
type Application struct {
server *plumego.Server
}
func NewApplication(cfg Config) *Application {
srv := plumego.NewServer(plumego.ServerConfig{
Addr: cfg.HTTPAddr,
})
registerMiddleware(srv, cfg)
registerRoutes(srv, cfg)
return &Application{server: srv}
}
This pattern ensures:
- Explicit dependency wiring
- Clear ownership of lifecycle
- Testable construction logic
Advanced Middleware Composition
Middleware in Plumego is intentionally simple, but composition becomes powerful when used deliberately.
Categorizing Middleware
A useful mental model is to group middleware into tiers:
-
Infrastructure Middleware
- Panic recovery
- Request ID injection
- Timeout enforcement
-
Observability Middleware
- Logging
- Tracing
- Metrics
-
Security Middleware
- Authentication
- Authorization
- Rate limiting
-
Business-Aware Middleware
- Tenant resolution
- Feature flags
- Localization
Explicit Ordering Example
srv.Use(recovery.Middleware())
srv.Use(requestid.Middleware())
srv.Use(tracing.Middleware())
srv.Use(logging.Middleware())
srv.Use(authenticate.Middleware())
srv.Use(authorize.Middleware())
In Plumego, order is documentation. Reviewing middleware order tells you exactly how a request flows.
Designing Context Contracts
One of the most important advanced topics in Plumego is context discipline.
Context Is a Contract, Not a Dumping Ground
A common anti-pattern in Go systems is unstructured context usage:
ctx = context.WithValue(ctx, "user", user)
In Plumego-based systems, define explicit context keys and accessors.
Example: Strongly-Typed Context Access
type UserContextKey struct{}
func WithUser(ctx context.Context, u *User) context.Context {
return context.WithValue(ctx, UserContextKey{}, u)
}
func UserFromContext(ctx context.Context) (*User, bool) {
u, ok := ctx.Value(UserContextKey{}).(*User)
return u, ok
}
This pattern:
- Avoids key collisions
- Makes dependencies explicit
- Improves code searchability
Plumego does not impose this pattern—but it strongly benefits from it.
Handler Design: Thin, Explicit, Boring
In advanced Plumego usage, handlers should be thin adapters, not logic containers.
Recommended Handler Responsibilities
Handlers should:
- Parse input
- Validate input
- Call a use case
- Translate output to transport format
They should not:
- Implement business rules
- Perform database logic
- Manage transactions
Example Handler
func CreateUserHandler(uc *usecase.CreateUser) plumego.HandlerFunc {
return func(ctx *plumego.Context) error {
var req CreateUserRequest
if err := ctx.BindJSON(&req); err != nil {
return err
}
user, err := uc.Execute(ctx.Context(), req)
if err != nil {
return err
}
return ctx.JSON(201, user)
}
}
This design makes handlers:
- Easy to test
- Easy to refactor
- Framework-agnostic
Error Translation as Middleware
Advanced systems require consistent error semantics.
Domain Errors vs Transport Errors
Define domain-level errors:
var ErrUserNotFound = errors.New("user not found")
Then translate them at the boundary:
func ErrorMappingMiddleware() plumego.Middleware {
return func(next plumego.HandlerFunc) plumego.HandlerFunc {
return func(ctx *plumego.Context) error {
err := next(ctx)
if err == nil {
return nil
}
switch err {
case ErrUserNotFound:
return ctx.JSON(404, ErrorResponse{Message: err.Error()})
default:
return ctx.JSON(500, ErrorResponse{Message: "internal error"})
}
}
}
}
This keeps:
- Domain logic pure
- Transport logic localized
- Error behavior predictable
Lifecycle Management and Graceful Shutdown
Production services must shut down cleanly.
Explicit Shutdown Handling
Plumego exposes server lifecycle hooks that allow you to coordinate shutdown:
- Stop accepting new connections
- Finish in-flight requests
- Close external resources
Coordinated Shutdown Pattern
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
go func() {
if err := srv.Start(); err != nil {
log.Println(err)
}
}()
<-ctx.Done()
srv.Shutdown(context.Background())
Advanced systems extend this by:
- Closing DB pools
- Flushing logs
- Draining queues
Observability in Advanced Plumego Systems
Plumego’s explicit nature makes observability easier, not harder.
Logging Strategy
- Log at boundaries
- Avoid logging in tight loops
- Attach request IDs explicitly
- Prefer structured logs
Tracing Strategy
- Start spans at ingress
- Propagate context explicitly
- Name spans after use cases
- Avoid framework-generated span names
Plumego’s lack of hidden behavior ensures your telemetry reflects your architecture, not the framework’s.
WebSocket Architecture Patterns
In advanced real-time systems, WebSocket handling must be disciplined.
Treat WebSockets as Stateful Sessions
Recommended practices:
- Explicit connection lifecycle
- Clear authentication at upgrade
- Context cancellation on disconnect
- Backpressure-aware message handling
Plumego’s explicit upgrade handling makes these patterns straightforward.
In-Process Pub-Sub for Domain Events
Advanced Plumego systems often use in-process pub-sub for domain events.
When to Use It
- Side effects (emails, notifications)
- Cache invalidation
- Audit logging
- Internal projections
When Not to Use It
- Cross-service communication
- Guaranteed delivery
- High-volume streams
Treat it as a local coordination mechanism, not a message broker.
Configuration Management Strategy
Advanced systems require disciplined configuration.
Principles
- Configuration is read-only at runtime
- Inject config at construction time
- Avoid global config access
- Validate config on startup
Plumego aligns well with these principles because construction is explicit.
Scaling Teams with Plumego
Plumego’s strongest advantage emerges as teams grow.
Why It Scales Organizationally
- Explicit boundaries reduce accidental coupling
- Code reviews are easier
- Architecture is visible in code
- Onboarding is clearer
Frameworks that hide behavior tend to accumulate “tribal knowledge.” Plumego externalizes that knowledge into code structure.
Common Advanced Mistakes
Even experienced teams can misuse Plumego.
Mistake 1: Recreating a “Magic Framework”
Adding:
- Global registries
- Implicit wiring
- Reflection-heavy helpers
This negates Plumego’s strengths.
Mistake 2: Over-Abstracting Too Early
Plumego works best when abstractions are introduced in response to real pressure, not in anticipation of it.
Final Thoughts: Plumego as an Engineering Tool
Plumego is not about speed in the small. It is about stability in the large.
Advanced usage of Plumego rewards teams who:
- Think in systems
- Value explicitness
- Design for longevity
- Accept responsibility for architecture
If you approach Plumego expecting it to solve architecture for you, it will disappoint you.
If you approach Plumego as a tool to help you express architecture clearly, it will serve you exceptionally well.
Plumego does not hide complexity.
It helps you face it, deliberately.