Service - Essential Guide
Service layer patterns and dependency injection
Time: 45 minutes (with examples) β’ Level: Beginner to Intermediate
π What Youβll Learn
- β Service factory pattern and registration
- β 3 ways to access services (and when to use each)
- β LazyLoad for performance (20-100x faster!) β
- β Service dependencies and injection
- β Service as Router - Auto-generate endpoints π (UNIQUE!)
- β Best practices for production code
π― What is a Service?
A Service in Lokstra is a business logic container that:
- Encapsulates domain logic (users, orders, payments, etc)
- Can be registered in the global registry
- Can be accessed by handlers and other services
- Can automatically generate REST endpoints (Service as Router!)
Key Insight: Services are the backbone of your application architecture.
π Quick Start (2 Minutes)
package main
import (
"github.com/primadi/lokstra"
"github.com/primadi/lokstra/lokstra_registry"
"github.com/primadi/lokstra/core/service"
)
// 1. Define service
type UserService struct {
users []User
}
func (s *UserService) GetAll() ([]User, error) {
return s.users, nil
}
// 2. Create factory
func NewUserService() (*UserService, error) {
return &UserService{
users: []User{
{ID: 1, Name: "Alice"},
{ID: 2, Name: "Bob"},
},
}, nil
}
// 3. Register service
func main() {
lokstra_registry.RegisterServiceFactory("users", NewUserService)
// 4. Access in handler (LazyLoad - recommended!)
var userService = service.LazyLoad[*UserService]("users")
r := lokstra.NewRouter("api")
r.GET("/users", func() ([]User, error) {
return userService.MustGet().GetAll()
})
app := lokstra.NewApp("demo", ":3000", r)
if err := app.Run(30 * time.Second); err != nil {
fmt.Println("Error starting server:", err)
}
}
π Service Registration Pattern
3 Ways to Register Services
Lokstra provides two registration methods with different characteristics:
| Method | When Created | Use Case | Factory Modes |
|---|---|---|---|
RegisterServiceFactory |
App startup (eager) | Simple services, always needed | 1 mode |
RegisterLazyService |
First access (lazy) | Complex deps, conditional use | 3 modes |
Step 1: Define Service Struct
type UserService struct {
db *Database // Dependencies injected
cache *Cache
}
func (s *UserService) GetAll() ([]User, error) {
// Business logic here
return s.db.Query("SELECT * FROM users")
}
func (s *UserService) GetByID(id int) (*User, error) {
return s.db.QueryOne("SELECT * FROM users WHERE id = ?", id)
}
func (s *UserService) Create(user *User) error {
return s.db.Insert("users", user)
}
Step 2a: Eager Registration (Simple Services)
Use RegisterServiceFactory for services that are always needed and have no complex dependencies:
func NewUserService() (*UserService, error) {
// Initialize service
db, err := ConnectDatabase()
if err != nil {
return nil, err
}
cache := NewCache()
return &UserService{
db: db,
cache: cache,
}, nil
}
func main() {
// Eager: Created immediately at app startup
lokstra_registry.RegisterServiceFactory("users", NewUserService)
app := lokstra.NewApp("myapp", ":8080", routers...)
if err := app.Run(30 * time.Second); err != nil {
fmt.Println("Error starting server:", err)
}
}
Characteristics:
- β Created at app startup (before routes activated)
- β Simpler for services without complex dependencies
- β
One factory signature:
func() (T, error) - β Canβt handle circular dependencies
- β All services created even if unused
Step 2b: Lazy Registration (Complex Dependencies) β RECOMMENDED
Use RegisterLazyService for services with dependencies or conditional usage:
func main() {
// Lazy: Created on first access, any order!
// Supports 3 factory modes:
// Mode 1: No params (simplest!)
lokstra_registry.RegisterLazyService("cache", func() any {
return NewCache()
}, nil)
// Mode 2: Config only
lokstra_registry.RegisterLazyService("db", func(cfg map[string]any) any {
dsn := cfg["dsn"].(string)
return ConnectDatabase(dsn)
}, map[string]any{
"dsn": "postgresql://localhost/mydb",
})
// Mode 3: Full signature (deps + config)
lokstra_registry.RegisterLazyService("users", func(deps, cfg map[string]any) any {
// Get dependencies from registry
db := lokstra_registry.MustGetService[*Database]("db")
cache := lokstra_registry.MustGetService[*Cache]("cache")
// Use config if needed
timeout := cfg["timeout"].(int)
return &UserService{db: db, cache: cache}
}, map[string]any{
"timeout": 30,
})
app := lokstra.NewApp("myapp", ":8080", routers...)
if err := app.Run(30 * time.Second); err != nil {
fmt.Println("Error starting server:", err)
}
}
Characteristics:
- β Created only when first accessed (lazy!)
- β Register in any order (auto dependency resolution)
- β 3 flexible factory signatures (choose simplest that fits)
- β Per-instance config (e.g., multiple DBs with different DSN)
- β Thread-safe singleton
- β Handles complex dependency graphs
- β οΈ Slightly more complex API
π Which to use?
- Simple services, no deps β
RegisterServiceFactory - Complex deps, conditional use β
RegisterLazyServiceβ - Most apps β Mix both! (see Example 03)
Step 3: Access Services (Same for Both!)
// Both methods accessed the same way:
var userService = service.LazyLoad[*UserService]("users")
func handler() (*response.ApiHelper, error) {
api := response.NewApiHelper()
users, err := userService.MustGet().GetAll()
// ...
}
β οΈ Important: Register services before NewApp(). Services are initialized during app creation (eager) or first access (lazy).
π 3 Ways to Access Services
Method 1: GetService() - Direct Registry Lookup
Use case: Dynamic service names, prototypes, optional services
r.GET("/users", func(ctx *request.Context) error {
// β οΈ Registry lookup EVERY request
userService := lokstra_registry.GetService[*UserService]("users")
// β οΈ Must check for nil
if userService == nil {
return ctx.Api.InternalError("Service not found")
}
users, err := userService.GetAll()
if err != nil {
return ctx.Api.InternalError(err.Error())
}
return ctx.Api.Ok(users)
})
Pros:
- β Simple and straightforward
- β Works for dynamic service names
- β Handles optional services
Cons:
- β Slow (map lookup every request)
- β Returns nil (confusing error messages)
- β Verbose (need nil check)
Performance: ~100-200ns overhead per call
Method 2: MustGetService() - Fail-Fast Lookup
Use case: Critical services, development, fail-fast behavior
r.GET("/users", func(ctx *request.Context) error {
// β οΈ Panics if service not found
userService := lokstra_registry.MustGetService[*UserService]("users")
users, err := userService.GetAll()
if err != nil {
return ctx.Api.InternalError(err.Error())
}
return ctx.Api.Ok(users)
})
Pros:
- β Clear error messages (panics with service name)
- β No nil checks needed
- β Fail-fast behavior
Cons:
- β Slow (map lookup every request)
- β Panics (not ideal for production APIs)
Performance: ~100-200ns overhead per call
Method 3: service.LazyLoad() - Cached Access β RECOMMENDED
Use case: Production code, high-traffic endpoints, package-level access
// Package-level: Cached after first access
var userService = service.LazyLoad[*UserService]("users")
r.GET("/users", func() (*response.ApiHelper, error) {
api := response.NewApiHelper()
// β
Cached! Only 1-5ns overhead
users, err := userService.MustGet().GetAll()
if err != nil {
api.InternalError(err.Error())
return api, nil
}
api.Ok(users)
return api, nil
})
Pros:
- β 20-100x faster (cached after first access!)
- β
Clear errors with
.MustGet() - β Clean code (no nil checks)
- β Production-ready
Cons:
- β οΈ Must be package-level or struct field (not function-local!)
Performance:
- First access: ~100-200ns (one-time)
- Subsequent: ~1-5ns (cached)
π¨ LazyLoad: MustGet() vs Get()
β Recommended: MustGet()
Clear error messages when service not found:
var userService = service.LazyLoad[*UserService]("users")
func handler(ctx *request.Context) error {
users, err := userService.MustGet().GetAll()
// If service not found:
// Panic: "service 'users' not found or not initialized"
// β
CLEAR! You know exactly what's wrong
}
β οΈ Not Recommended: Get()
Confusing nil pointer errors:
var userService = service.LazyLoad[*UserService]("users")
func handler(ctx *request.Context) error {
users, err := userService.Get().GetAll()
// If service not found:
// Panic: "runtime error: invalid memory address or nil pointer dereference"
// β CONFUSING! What caused nil? DB? Service? Something else?
}
When to use Get(): Only when you want custom nil handling:
svc := userService.Get()
if svc == nil {
log.Warn("Service not available, using fallback")
return fallbackResponse
}
users, err := svc.GetAll()
π Service Dependencies
Services can depend on other services:
type OrderService struct {
userService *UserService // Dependency
paymentService *PaymentService // Dependency
}
func NewOrderService() (*OrderService, error) {
// Get dependencies from registry
userSvc := lokstra_registry.MustGetService[*UserService]("users")
paymentSvc := lokstra_registry.MustGetService[*PaymentService]("payments")
return &OrderService{
userService: userSvc,
paymentService: paymentSvc,
}, nil
}
func (s *OrderService) CreateOrder(userID int, amount float64) (*Order, error) {
// Use dependencies
user, err := s.userService.GetByID(userID)
if err != nil {
return nil, err
}
payment, err := s.paymentService.Charge(user, amount)
if err != nil {
return nil, err
}
// Create order...
}
Registration order:
func main() {
// Register dependencies first
lokstra_registry.RegisterServiceFactory("users", NewUserService)
lokstra_registry.RegisterServiceFactory("payments", NewPaymentService)
// Then register dependent services
lokstra_registry.RegisterServiceFactory("orders", NewOrderService)
app := lokstra.NewApp("myapp", ":8080", routers...)
if err := app.Run(30 * time.Second); err != nil {
fmt.Println("Error starting server:", err)
}
}
π Service as Router (UNIQUE FEATURE!)
Automatically generate REST endpoints from service methods!
Traditional Approach (Manual)
type UserService struct {
users []User
}
func (s *UserService) GetAll() ([]User, error) { ... }
func (s *UserService) GetByID(id int) (*User, error) { ... }
func (s *UserService) Create(user *User) error { ... }
// Register service
lokstra_registry.RegisterServiceFactory("users", NewUserService)
// β Manually create router and handlers
r := lokstra.NewRouter("api")
r.GET("/users", handleGetAll)
r.GET("/users/{id}", handleGetByID)
r.POST("/users", handleCreate)
// Tedious!
Service as Router (Automatic!) β
type UserService struct {
users []User
}
func (s *UserService) GetAll() ([]User, error) { ... }
func (s *UserService) GetByID(id int) (*User, error) { ... }
func (s *UserService) Create(user *User) error { ... }
// Register service WITH config
lokstra_registry.RegisterServiceFactory("users", NewUserService)
lokstra_registry.RegisterServiceConfig("users", map[string]any{
"api.enabled": true,
"api.prefix": "/api/users",
})
// β
Auto-generate router!
userRouter := lokstra_registry.MustGetServiceAsRouter("users")
// Routes automatically created:
// GET /api/users β GetAll()
// GET /api/users/{id} β GetByID(id)
// POST /api/users β Create(user)
Benefits:
- β No boilerplate handler code
- β Type-safe automatically
- β Consistent API structure
- β Faster development
- β Less code to maintain
See Example 04 for full details!
π§ͺ Examples
All examples are runnable! Navigate to each folder and go run main.go
Total learning time: ~50 minutes
01 - Simple Service β±οΈ 10 min
Learn: Service registration, factory pattern, basic access
lokstra_registry.RegisterServiceFactory("users", NewUserService)
var userService = service.LazyLoad[*UserService]("users")
users, err := userService.MustGet().GetAll()
Key Concepts: Factory pattern, registration, LazyLoad, MustGet()
02 - LazyLoad vs GetService β±οΈ 12 min
Learn: Performance comparison, when to use each method
// Slow: GetService (100-200ns per call)
userService := lokstra_registry.GetService[*UserService]("users")
// Fast: LazyLoad (1-5ns after first access)
var userService = service.LazyLoad[*UserService]("users")
users := userService.MustGet().GetAll()
Key Concepts: Performance, benchmarking, best practices
03 - Service Dependencies β±οΈ 15 min β
Learn: Lazy registration, 3 factory modes, auto dependency resolution
// Register in ANY order! Dependencies auto-resolved
// Mode 1: No params (simplest)
lokstra_registry.RegisterLazyService("user-repo", func() any {
return repository.NewUserRepository()
}, nil)
// Mode 2: Config only
lokstra_registry.RegisterLazyService("db", func(cfg map[string]any) any {
return db.NewConnection(cfg["dsn"].(string))
}, map[string]any{"dsn": "postgresql://localhost/main"})
// Mode 3: Full signature (deps + config)
lokstra_registry.RegisterLazyService("order-service", func(deps, cfg map[string]any) any {
userSvc := lokstra_registry.MustGetService[*UserService]("user-service")
return service.NewOrderService(userSvc)
}, nil)
Key Concepts: LazyService registration, 3 factory modes, no ordering required, per-instance config
04 - Service as Router β±οΈ 20 min β
Learn: Auto-generate endpoints from service methods (UNIQUE!)
lokstra_registry.RegisterServiceConfig("users", map[string]any{
"api.enabled": true,
"api.prefix": "/api/users",
})
// Auto-generates:
// GET /api/users β GetAll()
// GET /api/users/{id} β GetByID(id)
// POST /api/users β Create(user)
// PUT /api/users/{id} β Update(id, user)
// DELETE /api/users/{id} β Delete(id)
Key Concepts: Code generation, convention over configuration, rapid development
π― Common Patterns
Pattern 1: Package-Level Service Access
package handlers
import "github.com/primadi/lokstra/core/service"
// Package-level: Shared by all handlers
var (
userService = service.LazyLoad[*UserService]("users")
orderService = service.LazyLoad[*OrderService]("orders")
)
func ListUsersHandler() (*response.ApiHelper, error) {
api := response.NewApiHelper()
users, err := userService.MustGet().GetAll()
if err != nil {
api.InternalError(err.Error())
return api, nil
}
api.Ok(users)
return api, nil
}
Pattern 2: Struct Field Service Access
type UserHandler struct {
userService *service.Cached[*UserService]
}
func NewUserHandler() *UserHandler {
return &UserHandler{
userService: service.LazyLoad[*UserService]("users"),
}
}
func (h *UserHandler) List(ctx *request.Context) error {
users, err := h.userService.MustGet().GetAll()
return ctx.Api.Ok(users)
}
Pattern 3: Service with Repository Pattern
type UserRepository interface {
FindAll() ([]User, error)
FindByID(id int) (*User, error)
Create(user *User) error
}
type UserService struct {
repo UserRepository
}
func NewUserService() (*UserService, error) {
return &UserService{
repo: NewPostgresUserRepository(),
}, nil
}
func (s *UserService) GetAll() ([]User, error) {
return s.repo.FindAll()
}
π« Common Mistakes
β Donβt: Use LazyLoad in Function Scope
func handler(ctx *request.Context) error {
// β Created every request! Cache useless!
userService := service.LazyLoad[*UserService]("users")
users, err := userService.MustGet().GetAll()
}
β Do: Use at package or struct level
// β
Package-level: Created once, cached forever
var userService = service.LazyLoad[*UserService]("users")
func handler(ctx *request.Context) error {
users, err := userService.MustGet().GetAll()
}
β Donβt: Register Services After App Creation
app := lokstra.NewApp("myapp", ":8080", routers...)
// β TOO LATE! Services already initialized
lokstra_registry.RegisterServiceFactory("users", NewUserService)
β Do: Register before app creation
// β
Register first
lokstra_registry.RegisterServiceFactory("users", NewUserService)
// Then create app
app := lokstra.NewApp("myapp", ":8080", routers...)
β Donβt: Ignore Factory Errors
func NewUserService() (*UserService, error) {
db, err := ConnectDatabase()
// β Ignoring error!
return &UserService{db: db}, nil
}
β Do: Propagate errors
func NewUserService() (*UserService, error) {
db, err := ConnectDatabase()
if err != nil {
return nil, fmt.Errorf("failed to connect database: %w", err)
}
return &UserService{db: db}, nil
}
π Best Practices
1. Always Use LazyLoad in Production
// β
Production: Fast, cached, clear errors
var userService = service.LazyLoad[*UserService]("users")
func handler(ctx *request.Context) error {
users, err := userService.MustGet().GetAll()
}
2. Use MustGet() for Clear Errors
// β
Clear error: "service 'users' not found"
users, err := userService.MustGet().GetAll()
// β Confusing error: "nil pointer dereference"
users, err := userService.Get().GetAll()
3. Keep Services Focused
// β
Good: Focused on users
type UserService struct {
repo UserRepository
}
func (s *UserService) GetAll() ([]User, error) { ... }
func (s *UserService) GetByID(id int) (*User, error) { ... }
func (s *UserService) Create(user *User) error { ... }
// β Bad: Too many responsibilities
type GodService struct {
userRepo UserRepository
orderRepo OrderRepository
paymentRepo PaymentRepository
}
4. Use Interfaces for Dependencies
// β
Good: Interface for testability
type UserService struct {
repo UserRepository // Interface
}
// Easy to mock in tests
func TestUserService(t *testing.T) {
mockRepo := &MockUserRepository{}
service := &UserService{repo: mockRepo}
// ...
}
// β Bad: Concrete dependency
type UserService struct {
repo *PostgresUserRepository // Hard to test
}
5. Register Services in Order
// β
Good: Dependencies first
lokstra_registry.RegisterServiceFactory("database", NewDatabase)
lokstra_registry.RegisterServiceFactory("cache", NewCache)
lokstra_registry.RegisterServiceFactory("users", NewUserService) // Depends on database
lokstra_registry.RegisterServiceFactory("orders", NewOrderService) // Depends on users
// β Bad: Random order (may fail)
lokstra_registry.RegisterServiceFactory("orders", NewOrderService) // Error: users not found
lokstra_registry.RegisterServiceFactory("users", NewUserService)
π Whatβs Next?
You now understand:
- β Service registration and factory pattern
- β 3 ways to access services (GetService, MustGetService, LazyLoad)
- β LazyLoad for production (20-100x faster!)
- β Service dependencies and injection
- β Service as Router (auto-generate endpoints!)
- β Best practices
Next Steps:
Continue Learning:
- π 03 - Middleware - Cross-cutting concerns
- π 04 - Configuration - Config-driven services
- π 05 - App and Server - Application lifecycle
Deep Dive Topics:
- Service Lifecycle (coming soon)
- Service as Router Details (coming soon)
- Testing Services (coming soon)
π Quick Reference
Registration
lokstra_registry.RegisterServiceFactory("name", factory)
lokstra_registry.RegisterServiceConfig("name", config)
Access Methods
// Slow, returns nil
GetService[T](name)
// Slow, panics
MustGetService[T](name)
// Fast, cached β
service.LazyLoad[T](name).MustGet()
service.LazyLoad[T](name).Get()
Service as Router
lokstra_registry.MustGetServiceAsRouter("name")
Continue learning β 03 - Middleware