Using Scopes
Scopes are essential for web applications. They isolate each request, ensuring data doesn’t leak between users.
What are Scopes?
A scope is a boundary for service instances. Services with Scoped lifetime get new instances per scope.
// One database connection for the app
godi.AddSingleton(NewDatabase)
// New transaction per request
godi.AddScoped(NewTransaction)
Basic Usage
Creating and Using Scopes
func HandleRequest(provider godi.ServiceProvider) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Create scope for this request
scope := provider.CreateScope(r.Context())
defer scope.Close() // Always clean up!
// Get services for this request
service, err := godi.Resolve[UserService](scope)
if err != nil {
http.Error(w, "Service error", 500)
return
}
// Use the service
service.ProcessRequest()
}
}
Real-World Example: Web API
Here’s a complete example showing scopes in action:
// Models
type RequestContext struct {
RequestID string
UserID string
StartTime time.Time
}
// Scoped service - new instance per request
func NewRequestContext(ctx context.Context) *RequestContext {
return &RequestContext{
RequestID: ctx.Value("requestID").(string),
UserID: ctx.Value("userID").(string),
StartTime: time.Now(),
}
}
// Transaction - also scoped
type Transaction struct {
tx *sql.Tx
ctx *RequestContext
}
func NewTransaction(db *Database, ctx *RequestContext) (*Transaction, error) {
tx, err := db.Begin()
if err != nil {
return nil, err
}
return &Transaction{
tx: tx,
ctx: ctx,
}, nil
}
func (t *Transaction) Commit() error {
duration := time.Since(t.ctx.StartTime)
log.Printf("[%s] Transaction committed after %v", t.ctx.RequestID, duration)
return t.tx.Commit()
}
// Repository using the transaction
type UserRepository struct {
tx *Transaction
}
func NewUserRepository(tx *Transaction) *UserRepository {
return &UserRepository{tx: tx}
}
func (r *UserRepository) SaveUser(user *User) error {
log.Printf("[%s] Saving user %s", r.tx.ctx.RequestID, user.ID)
// Use transaction for this request
_, err := r.tx.tx.Exec("INSERT INTO users ...")
return err
}
Module Setup
var WebModule = godi.NewModule("web",
// Singleton - shared
godi.AddSingleton(NewDatabase),
godi.AddSingleton(NewLogger),
// Scoped - per request
godi.AddScoped(NewRequestContext),
godi.AddScoped(NewTransaction),
godi.AddScoped(NewUserRepository),
godi.AddScoped(NewUserService),
)
HTTP Handler
func CreateUserHandler(provider godi.ServiceProvider) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Add request data to context
ctx := context.WithValue(r.Context(), "requestID", generateID())
ctx = context.WithValue(ctx, "userID", getUserID(r))
// Create scope with context
scope := provider.CreateScope(ctx)
defer scope.Close()
// Get service - it has access to request context!
service, err := godi.Resolve[*UserService](scope)
if err != nil {
http.Error(w, "Service error", 500)
return
}
// Process request
var req CreateUserRequest
json.NewDecoder(r.Body).Decode(&req)
user, err := service.CreateUser(req)
if err != nil {
http.Error(w, err.Error(), 400)
return
}
json.NewEncoder(w).Encode(user)
// When scope closes:
// 1. Transaction commits/rollbacks
// 2. Resources are cleaned up
// 3. Metrics are recorded
}
}
Automatic Disposal
When a scope closes, it automatically disposes services that implement these interfaces:
// Simple disposal
type Disposable interface {
Close() error
}
// Disposal with context (for graceful shutdown)
type DisposableWithContext interface {
Close(ctx context.Context) error
}
Disposal Example
// File handler that needs cleanup
type FileProcessor struct {
file *os.File
ctx *RequestContext
}
func NewFileProcessor(ctx *RequestContext) (*FileProcessor, error) {
file, err := os.Create(fmt.Sprintf("upload_%s.tmp", ctx.RequestID))
if err != nil {
return nil, err
}
return &FileProcessor{
file: file,
ctx: ctx,
}, nil
}
// Implements Disposable
func (f *FileProcessor) Close() error {
log.Printf("[%s] Cleaning up file", f.ctx.RequestID)
f.file.Close()
return os.Remove(f.file.Name())
}
// Database transaction with disposal
type Transaction struct {
tx *sql.Tx
committed bool
}
func (t *Transaction) Close() error {
if !t.committed {
return t.tx.Rollback() // Auto-rollback if not committed
}
return nil
}
func (t *Transaction) Commit() error {
t.committed = true
return t.tx.Commit()
}
Disposal Order
Services are disposed in reverse order of creation (LIFO):
// Creation order:
// 1. Logger (singleton - not disposed with scope)
// 2. Database connection (scoped)
// 3. Transaction (scoped)
// 4. Repository (scoped)
// 5. Service (scoped)
scope.Close()
// Disposal order:
// 1. Service
// 2. Repository
// 3. Transaction (auto-rollback if not committed)
// 4. Database connection
// (Logger remains - disposed with provider)
Context-Aware Disposal
For graceful shutdown with timeouts:
type GracefulService struct {
workers []*Worker
}
func (s *GracefulService) Close(ctx context.Context) error {
log.Println("Starting graceful shutdown...")
done := make(chan error, 1)
go func() {
// Stop all workers
for _, w := range s.workers {
w.Stop()
}
done <- nil
}()
select {
case err := <-done:
log.Println("Graceful shutdown complete")
return err
case <-ctx.Done():
log.Println("Shutdown timeout - forcing close")
return ctx.Err()
}
}
Scope Isolation
Each scope has its own instances:
func TestScopeIsolation(t *testing.T) {
module := godi.NewModule("test",
godi.AddScoped(func() *Counter {
return &Counter{value: 0}
}),
)
services := godi.NewServiceCollection()
services.AddModules(module)
provider, _ := services.BuildServiceProvider()
// Request 1
scope1 := provider.CreateScope(context.Background())
counter1, _ := godi.Resolve[*Counter](scope1.ServiceProvider())
counter1.Increment() // value = 1
// Request 2 - different instance!
scope2 := provider.CreateScope(context.Background())
counter2, _ := godi.Resolve[*Counter](scope2.ServiceProvider())
counter2.Increment() // value = 1 (not 2!)
// Different instances
assert.NotSame(t, counter1, counter2)
scope1.Close()
scope2.Close()
}
Advanced Patterns
Middleware with Scopes
func DIMiddleware(provider godi.ServiceProvider) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Create scope
scope := provider.CreateScope(r.Context())
defer scope.Close()
// Add scope to request context
ctx := context.WithValue(r.Context(), "scope", scope)
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}
}
// In handlers
func MyHandler(w http.ResponseWriter, r *http.Request) {
scope := r.Context().Value("scope").(godi.Scope)
service, _ := godi.Resolve[MyService](scope)
// Use service...
}
Unit of Work Pattern
type UnitOfWork struct {
tx *sql.Tx
committed bool
repositories map[string]interface{}
}
func NewUnitOfWork(db *Database) (*UnitOfWork, error) {
tx, err := db.Begin()
if err != nil {
return nil, err
}
return &UnitOfWork{
tx: tx,
repositories: make(map[string]interface{}),
}, nil
}
func (u *UnitOfWork) UserRepository() *UserRepository {
if repo, ok := u.repositories["user"]; ok {
return repo.(*UserRepository)
}
repo := &UserRepository{tx: u.tx}
u.repositories["user"] = repo
return repo
}
func (u *UnitOfWork) Commit() error {
u.committed = true
return u.tx.Commit()
}
// Automatic disposal
func (u *UnitOfWork) Close() error {
if !u.committed {
return u.tx.Rollback()
}
return nil
}
Best Practices
1. Always Close Scopes
// ✅ Good
scope := provider.CreateScope(ctx)
defer scope.Close()
// ❌ Bad
scope := provider.CreateScope(ctx)
// Missing close - memory leak!
2. One Scope Per Request
// ✅ Good - one scope per HTTP request
func Handler(provider godi.ServiceProvider) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
scope := provider.CreateScope(r.Context())
defer scope.Close()
// Handle entire request with this scope
}
}
// ❌ Bad - multiple scopes per request
func BadHandler(provider godi.ServiceProvider) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Don't create multiple scopes!
scope1 := provider.CreateScope(r.Context())
service1, _ := godi.Resolve[Service1](scope1.ServiceProvider())
scope1.Close()
scope2 := provider.CreateScope(r.Context())
service2, _ := godi.Resolve[Service2](scope2.ServiceProvider())
scope2.Close()
}
}
3. Pass Context Through Scopes
// Add request metadata
ctx := context.WithValue(r.Context(), "requestID", uuid.New())
ctx = context.WithValue(ctx, "userID", getUserID(r))
// Create scope with enriched context
scope := provider.CreateScope(ctx)
// Services can access context
func NewAuditLogger(ctx context.Context) *AuditLogger {
return &AuditLogger{
requestID: ctx.Value("requestID").(string),
userID: ctx.Value("userID").(string),
}
}
4. Implement Disposal for Resources
// ✅ Good - cleanup resources
type ResourceManager struct {
resources []io.Closer
}
func (r *ResourceManager) Close() error {
var errs []error
for _, res := range r.resources {
if err := res.Close(); err != nil {
errs = append(errs, err)
}
}
return errors.Join(errs...)
}
// ❌ Bad - leaking resources
type BadService struct {
file *os.File
conn net.Conn
// No Close method - resources leak!
}
Common Use Cases
1. Database Transactions
Each request gets its own transaction that commits/rollbacks with the scope.
2. Request Logging
Track all operations within a request with consistent request ID.
3. User Context
Ensure user permissions and identity are consistent throughout request.
4. Resource Cleanup
Automatically close files, connections, or other resources when request ends.
5. Metrics Collection
Measure request duration and collect metrics when scope closes.
Summary
Scopes are powerful for:
Isolating requests - Each user gets their own instances
Managing transactions - Automatic commit/rollback
Resource cleanup - Guaranteed disposal
Request context - Consistent data throughout request
Remember: Always create a scope for each operation and close it when done!