Service Layer Architecture
Overview
The service layer provides a comprehensive, standardized approach to handling all aspects of domain entity operations. Each entity receives a complete suite of specialized services that work together to handle different concerns.
Service Types
Every domain entity has these service types:
Data Services
- Fetchers - Data retrieval with filtering, pagination, and querying
- Handlers - CRUD operations with transaction support and validation
- Tables - Database-to-entity mapping with ESO (Entity Storage Object) pattern
Transformation Services
- Molders - Form-to-entity transformation with change detection
- Serializers - Entity-to-JSON conversion for API responses
- Stringers - Entity-to-string conversion for display purposes
Relationship Services
- Hydrators - Relationship loading with preset configurations
- Relationers - Batch relationship loading to prevent N+1 queries
Specialized Services
- Searchers - Full-text search capabilities with indexing
- Validators - Business rule validation
- Policies - Access control and authorization
Service Architecture Pattern
Complete Service Suite
// Example for Garden entity
type GardenServices struct {
// Core data services
GardenFetcher *fetchers.GardenFetcher
GardenHandler *handlers.GardenHandler
GardenTable *tables.GardenTable
// Transformation services
GardenMolder *molders.CreateGardenFormMolder
GardenSerializer *serializers.GardenSerializer
GardenStringer *stringers.GardenStringer
// Relationship services
GardenHydrator *hydrators.GardenHydrator
GardenRelationer *relationers.GardenRelationer
// Specialized services
GardenSearcher *searchers.GardenSearcher
GardenValidator *validators.GardenValidator
}
Service Interaction Patterns
Create Operation Flow
Form → Validator → Molder → Handler → Table → Database
↓
Entity
↓
Serializer → JSON Response
Read Operation Flow
Query → Fetcher → Table → Entity → Hydrator → Relationer
↓ ↓
Serializer String Representation
Update Operation Flow
Form → Validator → Molder → Handler → Table → Database
↓ ↓
Change Detection Transaction Management
Service Implementation Standards
Interface Consistency
All services of the same type follow identical interface patterns:
// Fetcher interface pattern (example)
type EntityFetcher interface {
FindAll(ctx context.Context, mod *fetcher.FetcherMod) ([]*Entity, error)
FindOne(ctx context.Context, mod *fetcher.FetcherMod) (*Entity, bool, error)
FindOneById(ctx context.Context, id string, mod *fetcher.FetcherMod) (*Entity, bool, error)
FindPage(ctx context.Context, mod *fetcher.FetcherMod) ([]*Entity, *fetcher.FetcherPagination, error)
}
// Handler interface pattern (example)
type EntityHandler interface {
Create(ctx context.Context, entity *Entity, mod *handler.HandlerMod) (*Entity, error)
Update(ctx context.Context, entity *Entity, mod *handler.HandlerMod) (*Entity, error)
Delete(ctx context.Context, entity *Entity, mod *handler.HandlerMod) error
}
Dependency Injection
Services are wired together through explicit dependency injection:
type GardenFetcher struct {
Fetcher *fetcher.Fetcher // Base functionality
GardenTable *tables.GardenTable // Entity mapping
}
type GardenHandler struct {
Handler *handler.Handler // Base functionality
GardenTable *tables.GardenTable // Entity mapping
Spy *spy.Spy // Operation logging
Time time.Time // Time utilities
}
Service Composition Patterns
Layered Service Usage
// Mae uses multiple services in coordination
func (this *CreateGardenMae) Execute(ctx context.Context, input *CreateGardenMaeInput) (*CreateGardenMaeOutput, error) {
// 1. Form validation
if err := this.GardenValidator.ValidateCreateForm(input.Form); err != nil {
return nil, err
}
// 2. Transform form to entity
garden := &mdl.Garden{}
garden, changes := this.GardenMolder.ToEntity(input.Form, garden)
// 3. Persist entity
createdGarden, err := this.GardenHandler.Create(ctx, garden, nil)
if err != nil {
return nil, err
}
// 4. Load relationships if needed
if input.IncludeRelationships {
err := this.GardenHydrator.OneViaPreset(ctx, createdGarden, "show", nil)
if err != nil {
return nil, err
}
}
return &CreateGardenMaeOutput{
Garden: createdGarden,
}, nil
}
Service Chaining
// Services can be chained for complex operations
func (this *GardenController) List(c *gin.Context) {
// 1. Fetch entities
gardens, pagination, err := this.GardenFetcher.FindPage(c, mod)
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
// 2. Hydrate relationships
err = this.GardenHydrator.ManyViaPreset(c, gardens, "list", nil)
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
// 3. Serialize for response
jsonData, err := this.GardenSerializer.SetToJson(gardens)
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
c.Data(200, "application/json", jsonData)
}
Service Configuration Patterns
Modifier Objects
Services use modifier objects to configure operations:
// Fetcher modification
mod := gardenFetcher.Mod()
mod.ExactStringValueFilter("user_id", "123")
mod.DescOrder("created_at")
mod.Page = 1
mod.PerPage = 25
// Handler modification
handlerMod := &handler.HandlerMod{
Tx: transaction, // Use existing transaction
Fields: []string{"name", "description"}, // Partial update
}
// Hydrator modification
hydratorMod := &hydrator.HydratorMod{}
hydratorMod.AddHydratingPath("plants")
hydratorMod.AddHydratingPath("garden_tasks", "assignee")
Service Presets
Services can define preset configurations:
// Hydrator presets
func (this *GardenHydrator) OneViaPreset(ctx context.Context, entity *mdl.Garden, preset string, mod *hydrator.HydratorMod) error {
mod = this.Hydrator.ModDefaulting(mod)
switch preset {
case "show":
mod.AddHydratingPath("plants")
mod.AddHydratingPath("garden_tasks")
mod.AddHydratingPath("plantations", "plants")
case "list":
mod.AddHydratingPath("user")
case "api":
mod.AddHydratingPath("plants", "plant_tasks")
}
return this.One(ctx, entity, mod)
}
Error Handling in Services
Service-Level Errors
// Services return structured errors
type ServiceError struct {
Service string
Operation string
Message string
Cause error
}
func (this *GardenFetcher) FindOneById(ctx context.Context, id string, mod *fetcher.FetcherMod) (*mdl.Garden, bool, error) {
if id == "" {
return nil, false, &ServiceError{
Service: "GardenFetcher",
Operation: "FindOneById",
Message: "ID parameter is required",
}
}
// ... implementation
}
Error Propagation
// Services propagate errors with context
func (this *CreateGardenMae) Execute(ctx context.Context, input *CreateGardenMaeInput) (*CreateGardenMaeOutput, error) {
garden, err := this.GardenHandler.Create(ctx, entity, nil)
if err != nil {
return nil, fmt.Errorf("failed to create garden in CreateGardenMae: %w", err)
}
return output, nil
}
Testing Service Layer
Service Unit Testing
func TestGardenHandler_Create(t *testing.T) {
// Setup service with mock dependencies
handler := &handlers.GardenHandler{
Handler: mockHandler,
GardenTable: mockTable,
}
// Test creation
garden := &mdl.Garden{Name: "Test Garden"}
result, err := handler.Create(ctx, garden, nil)
// Assertions
require.NoError(t, err)
assert.NotEmpty(t, result.Id)
assert.Equal(t, "Test Garden", result.Name)
}
Service Integration Testing
func TestGardenServices_Integration(t *testing.T) {
// Setup real database and services
db := setupTestDatabase(t)
services := setupGardenServices(t, db)
// Test service interaction
form := &forms.CreateGardenForm{Name: "Integration Test"}
garden := &mdl.Garden{}
garden, _ = services.GardenMolder.ToEntity(form, garden)
created, err := services.GardenHandler.Create(ctx, garden, nil)
require.NoError(t, err)
found, exists, err := services.GardenFetcher.FindOneById(ctx, created.Id, nil)
require.NoError(t, err)
assert.True(t, exists)
assert.Equal(t, created.Name, found.Name)
}
Performance Considerations
Service Optimization
- Batch Operations - Use batch methods when available
- Connection Pooling - Proper database connection management
- Query Optimization - Efficient query patterns in Fetchers
- Memory Management - Avoid loading large datasets unnecessarily
- Caching Strategy - Cache frequently accessed data
Monitoring and Observability
// Services integrate with monitoring
type GardenHandler struct {
Handler *handler.Handler
Spy *spy.Spy // Operation logging
Metrics *metrics.Metrics // Performance metrics
}
func (this *GardenHandler) Create(ctx context.Context, entity *mdl.Garden, mod *handler.HandlerMod) (*mdl.Garden, error) {
start := time.Now()
defer func() {
this.Metrics.RecordDuration("garden.handler.create", time.Since(start))
}()
this.Spy.LogOperation(ctx, "GardenHandler.Create", entity.Id)
// ... implementation
}
LLM Service Development Notes
When working with the service layer: 1. Complete Coverage - Always implement full service suite for entities 2. Consistent Interfaces - Follow established patterns for each service type 3. Explicit Dependencies - Wire all dependencies through constructor injection 4. Error Handling - Provide structured errors with operation context 5. Testing Strategy - Test services both in isolation and integration 6. Performance Awareness - Consider query efficiency and resource usage 7. Monitoring Integration - Include logging and metrics in service operations