go-architecture
npx skills add https://github.com/direktly/agent-skills --skill go-architecture
Agent 安装分布
Skill 文档
Go Architecture Design
This guide explains the DDD and CQRS principles demonstrated in this codebase, presented in a way that allows engineers to apply these patterns to any industry or business domain.
Table of Contents
- Domain-Driven Design Overview
- Architecture Layers
- Domain Layer Principles
- Application Layer Patterns
- Infrastructure Layer Design
- CQRS Implementation
- Idempotency Pattern
- Best Practices
- Applying These Principles
Domain-Driven Design Overview
Domain-Driven Design (DDD) is a software development approach that:
- Places the business domain at the heart of the software
- Creates a shared language between developers and domain experts
- Isolates business logic from technical concerns
- Enables scalable and maintainable architectures
Core Concepts
- Ubiquitous Language: Use the same terminology in code that domain experts use
- Bounded Contexts: Define clear boundaries where specific domain models apply
- Entities: Objects with unique identity that persist over time
- Value Objects: Immutable objects defined by their attributes
- Aggregates: Clusters of entities and value objects with defined boundaries
- Repositories: Abstractions for data persistence
Architecture Layers
This implementation follows the Onion Architecture pattern with clear separation of concerns:
âââââââââââââââââââââââââââââââââââââââ
â Interface Layer â â External APIs, Controllers
âââââââââââââââââââââââââââââââââââââââ¤
â Application Layer â â Use Cases, Commands, Queries
âââââââââââââââââââââââââââââââââââââââ¤
â Domain Layer â â Business Logic, Entities
âââââââââââââââââââââââââââââââââââââââ¤
â Infrastructure Layer â â Database, External Services
âââââââââââââââââââââââââââââââââââââââ
Layer Dependencies
- Inner layers know nothing about outer layers
- Domain layer has zero dependencies on other layers
- Infrastructure implements interfaces defined by domain
- Application layer orchestrates between domain and infrastructure
Domain Layer Principles
1. Entity Design
type Entity struct {
ID uuid.UUID // Always use unique identifiers
CreatedAt time.Time // Set by domain, not database
UpdatedAt time.Time // Updated on modifications
// Business attributes
}
Key Principles:
- Entities have identity that persists across time
- Factory methods (NewEntity) ensure valid initial state
- Business rules are enforced through methods
- Validation happens at creation and modification
2. Validation Pattern
// Private validation method
func (e *Entity) validate() error {
// Business rule validations
if e.BusinessAttribute == "" {
return errors.New("business rule violation")
}
return nil
}
// Public modification method with validation
func (e *Entity) UpdateAttribute(value string) error {
// Validate BEFORE modifying state
if value == "" {
return errors.New("business rule violation")
}
// Only modify if validation passes
e.BusinessAttribute = value
e.UpdatedAt = time.Now()
return nil
}
3. Validated Entity Pattern
type ValidatedEntity struct {
Entity
isValidated bool
}
func NewValidatedEntity(entity *Entity) (*ValidatedEntity, error) {
if err := entity.validate(); err != nil {
return nil, err
}
return &ValidatedEntity{
Entity: *entity,
isValidated: true,
}, nil
}
Purpose: Ensures only valid entities can be persisted
4. Repository Interfaces
type EntityRepository interface {
Create(entity *ValidatedEntity) (*Entity, error)
FindByID(id uuid.UUID) (*Entity, error)
FindAll() ([]*Entity, error)
Update(entity *ValidatedEntity) (*Entity, error)
Delete(id uuid.UUID) error
}
Key Points:
- Domain defines interfaces, infrastructure implements them
- Methods accept validated entities for writes
- Read methods return regular entities (not validated)
- Always return fresh data after writes
Application Layer Patterns
1. Service Structure
type EntityService struct {
repo repositories.EntityRepository
idempotencyRepo repositories.IdempotencyRepository
}
Services orchestrate:
- Command execution
- Query handling
- Transaction boundaries
- Cross-aggregate operations
2. Use Case Implementation
- Each use case is a method on the service
- Clear input (commands) and output (results)
- Handles idempotency
- Coordinates between repositories
Infrastructure Layer Design
1. Repository Implementation
type SqlcEntityRepository struct {
queries *db.Queries
}
func (repo *SqlcEntityRepository) Create(entity *ValidatedEntity) (*Entity, error) {
ctx := context.Background()
dbEntity, err := repo.queries.CreateEntity(ctx, db.CreateEntityParams{
ID: entity.ID,
Name: entity.Name,
CreatedAt: timestamptzFromTime(entity.CreatedAt),
UpdatedAt: timestamptzFromTime(entity.UpdatedAt),
})
if err != nil {
return nil, err
}
// Always read after write
return repo.FindByID(dbEntity.ID)
}
2. Mapping Pattern
// Domain to Database
func toDBModel(entity *ValidatedEntity) *DBModel {
// Map domain entity to database model
}
// Database to Domain
func fromDBModel(dbModel *DBModel) *Entity {
// Map database model to domain entity
}
Purpose: Keep domain models pure and database concerns isolated
CQRS Implementation
Command Pattern
Commands modify state and are task-oriented:
type CreateEntityCommand struct {
IdempotencyKey string
// Business attributes
}
type CreateEntityCommandResult struct {
Result *EntityResult
}
Query Pattern
Queries retrieve data without side effects:
// For queries with parameters
type GetEntityByIDQuery struct {
ID uuid.UUID
}
type GetEntityByIDQueryResult struct {
Result *EntityResult
}
// For simple parameterless queries, use direct method calls
func (s *EntityService) FindAllEntities() (*EntityQueryListResult, error) {
// Simple queries don't need query objects
}
// For complex queries with filters/parameters, use query objects
func (s *EntityService) FindEntitiesByCategory(query *GetEntitiesByCategoryQuery) (*EntityQueryListResult, error) {
// Complex queries benefit from query objects
}
Query Object Guidelines:
- Use query objects for queries with parameters or complex filters
- Simple parameterless queries (like FindAll) can use direct method calls
- This avoids unnecessary empty struct instantiation
Benefits of CQRS
- Optimized Read/Write Models: Different models for different purposes
- Scalability: Scale reads and writes independently
- Performance: Optimize queries without affecting write logic
- Clarity: Clear separation of intentions
Idempotency Pattern
Implementation
// Check for existing execution
if command.IdempotencyKey != "" {
existing, err := idempotencyRepo.FindByKey(ctx, command.IdempotencyKey)
if existing != nil {
return cachedResponse, nil
}
}
// Execute business logic
result := executeBusinessLogic()
// Store result for future requests
if command.IdempotencyKey != "" {
record := NewIdempotencyRecord(command.IdempotencyKey, request)
record.SetResponse(response, statusCode)
idempotencyRepo.Create(ctx, record)
}
Benefits
- Prevents duplicate operations
- Handles network failures gracefully
- Ensures consistency in distributed systems
Best Practices
1. Domain Layer Purity
- No framework dependencies
- No infrastructure concerns
- Business logic only
- Self-contained validation
2. Factory Methods
func NewEntity(businessAttribute string) *Entity {
return &Entity{
ID: uuid.New(),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
BusinessAttribute: businessAttribute,
}
}
3. Read After Write
Always return fresh data from the database after modifications to ensure consistency.
4. Historical Data Compatibility
- Don’t validate on read operations
- Allow loading of data created with old business rules
- Validate only on write operations
5. Soft Delete Pattern
Implement soft deletes at the infrastructure layer without polluting domain entities:
Domain Layer (Pure):
type Entity struct {
ID uuid.UUID
Name string
CreatedAt time.Time
UpdatedAt time.Time
// No DeletedAt field - keep domain pure
}
Infrastructure Layer (Database):
-- Database table includes deleted_at
CREATE TABLE entities (
id UUID PRIMARY KEY,
name TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL,
deleted_at TIMESTAMP WITH TIME ZONE -- Only in database
);
-- Delete operation becomes an UPDATE
-- name: DeleteEntity :exec
UPDATE entities SET deleted_at = NOW() WHERE id = $1;
-- All SELECT queries filter out soft-deleted records
-- name: GetEntityByID :one
SELECT id, name, created_at, updated_at
FROM entities
WHERE id = $1 AND deleted_at IS NULL;
Benefits:
- Domain entities remain focused on business logic
- Soft delete is an infrastructure concern, not a business rule
- Data recovery is possible without domain knowledge
- Audit trails are maintained at the database level
- Foreign key relationships remain intact
Implementation Guidelines:
- Add
deleted_atcolumn only in database schema - Update DELETE operations to set
deleted_at = NOW() - Add
WHERE deleted_at IS NULLto all SELECT queries - Repository implementations remain unchanged
- Domain layer is completely unaware of soft delete mechanism
Applying These Principles
Step 1: Identify Your Domain
- Work with domain experts to understand the business
- Identify key entities and their relationships
- Define business rules and invariants
- Create a ubiquitous language
Step 2: Design Your Entities
// Example for an e-commerce domain
type Order struct {
ID uuid.UUID
CreatedAt time.Time
UpdatedAt time.Time
CustomerID uuid.UUID
Items []OrderItem
Status OrderStatus
Total Money
}
func NewOrder(customerID uuid.UUID) *Order {
return &Order{
ID: uuid.New(),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
CustomerID: customerID,
Status: OrderStatusPending,
Items: []OrderItem{},
}
}
func (o *Order) AddItem(product Product, quantity int) error {
// Business logic for adding items
// Validate quantity, calculate prices, etc.
}
Step 3: Define Repository Interfaces
type OrderRepository interface {
Create(order *ValidatedOrder) (*Order, error)
FindByID(id uuid.UUID) (*Order, error)
FindByCustomerID(customerID uuid.UUID) ([]*Order, error)
Update(order *ValidatedOrder) (*Order, error)
}
Step 4: Implement CQRS
Commands:
type PlaceOrderCommand struct {
IdempotencyKey string
CustomerID uuid.UUID
Items []OrderItemRequest
}
Queries:
type GetCustomerOrdersQuery struct {
CustomerID uuid.UUID
Status *OrderStatus // Optional filterfunc NewEntity(businessAttribute string) *Entity {
return &Entity{
ID: uuid.New(),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
BusinessAttribute: businessAttribute,
}
}
}
Step 5: Create Application Services
type OrderService struct {
orderRepo repositories.OrderRepository
productRepo repositories.ProductRepository
idempotencyRepo repositories.IdempotencyRepository
}
func (s *OrderService) PlaceOrder(cmd *PlaceOrderCommand) (*PlaceOrderResult, error) {
// Implement idempotency check
// Validate products exist
// Create order
// Calculate totals
// Save order
// Return result
}
Industry-Agnostic Guidelines
- Healthcare: Patient (Entity), Appointment (Entity), Diagnosis (Value Object)
- Finance: Account (Entity), Transaction (Entity), Money (Value Object)
- Education: Student (Entity), Course (Entity), Grade (Value Object)
- Logistics: Shipment (Entity), Package (Entity), Address (Value Object)
The patterns remain the same; only the domain concepts change.
Conclusion
These DDD and CQRS principles provide a robust foundation for building maintainable, scalable applications regardless of your business domain. The key is to:
- Keep domain logic pure and isolated
- Use CQRS to separate read and write concerns
- Implement proper validation and factory patterns
- Handle idempotency for distributed systems
- Follow the dependency rules between layers
By applying these principles, you create software that clearly expresses business requirements while remaining flexible for future changes.