A Go library implementing the Command/Query pattern with type-safe operation creation, service injection, centralized logging, and flexible dependency management.
- Clean Command/Query Separation: Distinct interfaces for read and write operations
- Type-Safe Service Injection: Automatic dependency injection using Go generics
- Flexible Dependencies Access: Optional Dependencies injection with context-based access
- Context-Enriched Execution: Operations receive enriched context with metadata and Dependencies
- Centralized Logging: Built-in operation lifecycle logging with structured data
- Serializable Operations: Support for operation persistence and reconstruction
- Framework/Domain Separation: Reusable framework with user-defined domain logic
The framework follows the Command Pattern with four distinct roles:
┌─────────────┐ creates ┌─────────────┐
│ Client │──────────────►│ Command │
│ (CLI Parser)│ │ (Query) │
└─────────────┘ └─────────────┘
│
▼
┌──────────────┐ holds ref ┌─────────────┐ calls ┌─────────────┐
│ Invoker │──────────────►│ Command │────────────►│ Receiver │
│(OperationBus)│ │ (Query) │ │ (Service) │
└──────────────┘ └─────────────┘ └─────────────┘
go get github.com/davidlee/commandment/pkg/commandmentpackage myapp
import "context"
// Define your service interfaces
type UserService interface {
CreateUser(ctx context.Context, params CreateUserParams) (User, error)
GetUser(ctx context.Context, params GetUserParams) (User, error)
}// Parameters for operations
type CreateUserParams struct {
Name string
Email string
}
type GetUserParams struct {
ID int64
}
// Domain objects
type User struct {
ID int64
Name string
Email string
}import "github.com/davidlee/commandment/pkg/commandment"
// Command for creating users (mutates state)
type CreateUserCommand struct {
Params CreateUserParams
Service UserService
Meta commandment.OperationMetadata
Logger commandment.Logger
}
func (c *CreateUserCommand) Execute(ctx context.Context) (User, error) {
return commandment.ExecuteOperation(ctx, c, func(ctx context.Context) (User, error) {
return c.Service.CreateUser(ctx, c.Params)
})
}
func (c *CreateUserCommand) Metadata() commandment.OperationMetadata {
return c.Meta
}
func (c *CreateUserCommand) Descriptor() commandment.OperationDescriptor {
return commandment.OperationDescriptor{
Type: "CreateUserCommand",
Params: c.Params,
Metadata: c.Meta,
}
}
func (c *CreateUserCommand) GetMetadata() *commandment.OperationMetadata { return &c.Meta }
func (c *CreateUserCommand) GetLogger() commandment.Logger { return c.Logger }
// Query for getting users (read-only)
type GetUserQuery struct {
Params GetUserParams
Service UserService
Meta commandment.OperationMetadata
Logger commandment.Logger
}
// Implement similar methods...type UserBus struct {
bus *commandment.OperationBus
}
func NewUserBus(bus *commandment.OperationBus) *UserBus {
return &UserBus{bus: bus}
}
func (b *UserBus) NewCreateUserCommand(params CreateUserParams) (*CreateUserCommand, error) {
return commandment.CreateOperation[*CreateUserCommand](b.bus, params)
}
func (b *UserBus) NewGetUserQuery(params GetUserParams) (*GetUserQuery, error) {
return commandment.CreateOperation[*GetUserQuery](b.bus, params)
}// Setup framework
registry := commandment.NewServiceRegistry()
commandment.RegisterService[UserService](registry, myUserService)
logger := myLogger // implement commandment.Logger interface
operationBus := commandment.NewOperationBus(registry, logger)
userBus := NewUserBus(operationBus)
// Use operations
cmd, err := userBus.NewCreateUserCommand(CreateUserParams{
Name: "John Doe",
Email: "john@example.com",
})
if err != nil {
log.Fatal(err)
}
user, err := cmd.Execute(context.Background())
if err != nil {
log.Fatal(err)
}
fmt.Printf("Created user: %+v\n", user)See the /examples directory for complete working examples:
examples/nodemanager/- Complete domain implementation for node/tree managementexamples/basic/- Simple demo showing framework usage
Run the demo:
just demo
# or
cd examples/basic && go run main.gopkg/commandment/- Core framework that users importoperation.go- Base interfaces, metadata, and context enrichmentbus.go- Operation bus, creation logic, and Dependencies managementregistry.go- Service registry with type-safe injection
- Services - Define your business service interfaces
- Operations - Implement concrete commands/queries
- Parameters - Define parameter structs and domain objects
- Invokers - Create domain-specific operation buses
- Operations encapsulate a single unit of work with logging and metadata
- Services contain the actual business logic
- Operations use services but add framework capabilities
- Commands mutate state (implement
Command[T]interface) - Queries are read-only (implement
Query[T]interface) - Both share common
Operation[T]behavior
- Services are registered once in the
ServiceRegistry - Operations get services injected automatically during creation
- Type-safe service discovery using Go generics
- Every operation gets UUID, timestamps, and structured logging
- Operations log creation, execution start/end, and errors
- Full audit trail for all operations
Operations receive an enriched context during execution that includes operation metadata and optional Dependencies:
func (c *CreateUserCommand) Execute(ctx context.Context) (User, error) {
return commandment.ExecuteOperation(ctx, c, func(enrichedCtx context.Context) (User, error) {
// Access operation metadata from context
metadata := commandment.OperationMetadataFromContext(enrichedCtx)
if metadata != nil {
c.Logger.Info("Operation started", "operation_id", metadata.UUID)
}
// Your business logic with enriched context
return c.Service.CreateUser(enrichedCtx, c.Params)
})
}The framework supports flexible Dependencies injection for complex infrastructure needs:
// Define your application's Dependencies
type MyDependencies struct {
db *sql.DB
eventStore EventStore
logger Logger
}
func (d *MyDependencies) NodeRepository() NodeRepository {
return NewNodeRepository(d.db, d.logger)
}
func (d *MyDependencies) WithTransaction(fn func(*MyDependencies) error) error {
tx, err := d.db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
txDeps := &MyDependencies{db: tx, eventStore: d.eventStore, logger: d.logger}
err = fn(txDeps)
if err != nil {
return err
}
return tx.Commit()
}// Initialize your Dependencies
deps := &MyDependencies{
db: initDB(),
eventStore: initEventStore(),
logger: initLogger(),
}
// Create bus with default Dependencies
registry := commandment.NewServiceRegistry()
commandment.RegisterService[UserService](registry, myUserService)
bus := commandment.NewOperationBusWithDefaultDependencies(registry, logger, deps)Context-Based Access (Recommended):
func (c *CreateUserCommand) Execute(ctx context.Context) (User, error) {
return commandment.ExecuteOperation(ctx, c, func(ctx context.Context) (User, error) {
// Access Dependencies from enriched context
deps := commandment.DependenciesFromContext(ctx).(*MyDependencies)
return deps.WithTransaction(func(txDeps *MyDependencies) error {
repo := txDeps.NodeRepository()
eventWriter := txDeps.EventWriter()
// Complex operations with direct Dependencies access
return c.createUserWithEvents(repo, eventWriter, c.Params)
})
})
}Direct Access:
func (c *CreateUserCommand) Execute(ctx context.Context) (User, error) {
// Access Dependencies directly from operation
deps := commandment.GetDependencies(c).(*MyDependencies)
return commandment.ExecuteOperation(ctx, c, func(ctx context.Context) (User, error) {
return deps.WithTransaction(func(txDeps *MyDependencies) error {
// Use Dependencies for infrastructure concerns
return c.Service.CreateUser(ctx, c.Params)
})
})
}// Special Dependencies for specific operations
migrationDeps := &MigrationDependencies{
sourceDB: sourceDB,
targetDB: targetDB,
}
// Create operation with specific Dependencies
op, err := commandment.CreateOperationWithDependencies[*MigrateUsersCommand](
bus,
migrationParams,
migrationDeps,
)The framework supports multiple patterns for different use cases:
Pattern 1: Service-Only (Simple)
- Use service injection for domain logic
- No Dependencies needed for simple operations
Pattern 2: Direct Dependencies (Complex Infrastructure)
- Access Dependencies directly for transaction management
- Ideal for bulk operations, migrations, complex queries
Pattern 3: Hybrid (Flexible)
- Use Dependencies for infrastructure (transactions, caching)
- Use Services for domain logic
- Best of both approaches
# Run tests
just test
# Run linter
just lint
# Run demo
just demo
# Build demo binary
just build- Maintainable: Changes to logging/metadata have zero blast radius
- Type Safe: Compile-time guarantees, no runtime type assertions needed
- Testable: Easy to mock services and test operations in isolation
- Extensible: Adding new operations requires minimal boilerplate
- Observable: Rich structured logging with operation correlation
- Publishable: Clean separation between framework and domain code
MIT License - see LICENSE file for details.