Dew streamlines Go application development by providing a unified interface for handling operations and domain logic. It offers a lightweight command bus with an integrated middleware system, simplifying complex workflows and promoting clean, maintainable code architecture.
- Features
- Concept Diagram
- Motivation
- Terminology
- Convention for Actions and Queries
- Installation
- Example
- Usage
- Testing
- Benchmarks
- Contributing
- License
- Lightweight: Clocks around 450 LOC with minimalistic design.
- Pragmatic and Ergonomic: Focused on developer experience and productivity.
- Production Ready: 100% test coverage.
- Zero Dependencies: No external dependencies.
- Fast: See benchmarks.
The following diagram illustrates the core concepts and flow of Dew:
graph LR
Client[Client Code]
Bus[Dew Bus]
Middleware{Middleware}
ActionHandler[Action Handler]
QueryHandler[Query Handler]
subgraph Dew["Dew Command Bus"]
Bus
Middleware
end
Client -->|1. Dispatch Action| Bus
Client -->|2. Execute Query| Bus
Bus -->|3. Apply| Middleware
Middleware -->|4a. Route Action| ActionHandler
Middleware -->|4b. Route Query| QueryHandler
ActionHandler -->|5a. Handle & Modify State| DB[(Database)]
QueryHandler -->|5b. Fetch Data| DB
ActionHandler & QueryHandler -->|6. Return Result| Client
style Dew fill:#e6f3ff,stroke:#333,stroke-width:2px,color:#000
style Bus fill:#b3e0ff,stroke:#333,stroke-width:2px,color:#000
style Middleware fill:#ffd966,stroke:#333,stroke-width:2px,color:#000
style ActionHandler fill:#d5e8d4,stroke:#333,stroke-width:2px,color:#000
style QueryHandler fill:#d5e8d4,stroke:#333,stroke-width:2px,color:#000
style Client fill:#dae8fc,stroke:#333,stroke-width:2px,color:#000
style DB fill:#f8cecc,stroke:#333,stroke-width:2px,color:#000
Working on multiple complex backend applications in Go over the years, I've been seeking ways to enhance code readability, maintainability, and developer enjoyment. The Command Bus architecture emerged as a promising solution to these challenges. However, unable to find a library that met all my requirements, I created Dew.
Dew is designed to be lightweight and dependency-free, facilitating easy integration into any Go project. It implements the command-oriented interface pattern, promoting separation of concerns, modularization, and improved code readability while reducing cognitive load.
Dew uses four key concepts:
- Action: An operation that changes the application state. Similar to a "Command" in CQRS patterns.
- Query: An operation that retrieves data without modifying the application state.
- Middleware: A function that processes Actions and Queries before and/or after they are handled. Used for cross-cutting concerns like logging, authorization, or transactions.
- Bus: The central component that manages Actions, Queries, and Middleware. It routes operations to their appropriate handlers.
Dew follows these conventions for Action
and Query
interfaces:
- Action Interface: Each action must implement a
Validate
method to ensure the action's data is valid before processing. - Query Interface: Each query implements the
Query
interface, which is an empty interface. Queries don't require aValidate
method as they don't modify application state.
Example:
// MyAction represents an Action
type MyAction struct {
Amount int
}
// Validate implements the Action interface
func (a *MyAction) Validate(ctx context.Context) error {
if a.Amount <= 0 {
return fmt.Errorf("amount must be greater than zero")
}
return nil
}
// MyQuery represents a Query
type MyQuery struct {
AccountID string
}
// MyQuery doesn't need a Validate method as it doesn't change state
go get github.com/go-dew/dew
See examples for more detailed examples.
Basic usage:
package main
import (
"context"
"fmt"
"github.com/go-dew/dew"
)
// HelloAction is a simple action that greets the user.
type HelloAction struct {
Name string
}
// Validate checks if the name is valid.
func (c HelloAction) Validate(_ context.Context) error {
if c.Name == "" {
return fmt.Errorf("invalid name")
}
return nil
}
func main() {
// Initialize the Command Bus.
bus := dew.New()
// Register the handler for the HelloAction.
bus.Register(new(HelloHandler))
// Create a context with the bus.
ctx := dew.NewContext(context.Background(), bus)
// Dispatch the action.
result, err := dew.Dispatch(ctx, &HelloAction{Name: "Dew"})
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Printf("Result: %+v\n", result)
}
}
type HelloHandler struct {}
func (h *HelloHandler) HandleHelloAction(ctx context.Context, cmd *HelloAction) error {
fmt.Printf("Hello, %s!\n", cmd.Name) // Output: Hello, Dew!
return nil
}
Create a bus and register handlers:
bus := dew.New()
bus.Register(new(MyHandler))
Use the Dispatch
function to send actions:
ctx := dew.NewContext(context.Background(), bus)
result, err := dew.Dispatch(ctx, &MyAction{Message: "Hello, Dew!"})
if err != nil {
fmt.Println("Error dispatching action:", err)
} else {
fmt.Printf("Action result: %+v\n", result)
}
Use the Query
function to execute queries:
ctx := dew.NewContext(context.Background(), bus)
result, err := dew.Query(ctx, &MyQuery{Question: "What is Dew?"})
if err != nil {
fmt.Println("Error executing query:", err)
} else {
fmt.Printf("Query result: %+v\n", result)
}
Use QueryAsync
for handling multiple queries concurrently:
ctx := dew.NewContext(context.Background(), bus)
accountQuery := &AccountQuery{AccountID: "12345"}
weatherQuery := &WeatherQuery{City: "New York"}
err := dew.QueryAsync(ctx, dew.NewQuery(accountQuery), dew.NewQuery(weatherQuery))
if err != nil {
fmt.Println("Error executing queries:", err)
} else {
fmt.Println("Account Balance for ID 12345:", accountQuery.Result)
fmt.Println("Weather in New York:", weatherQuery.Result)
}
Middleware can be used to execute logic before and after command or query execution:
func loggingMiddleware(next dew.Middleware) dew.Middleware {
return dew.MiddlewareFunc(func(ctx dew.Context) error {
fmt.Println("Before executing command")
err := next.Handle(ctx)
fmt.Println("After executing command")
return err
})
}
func main() {
bus := dew.New()
bus.Use(dew.ACTION, loggingMiddleware)
bus.Register(new(MyCommandHandler))
}
Here's an example of a middleware that manages database transactions:
package main
import (
"context"
"fmt"
"github.com/go-dew/dew"
"database/sql"
)
// TransactionalMiddleware creates a middleware for handling transactions
func TransactionalMiddleware(db *sql.DB) func(next dew.Middleware) dew.Middleware {
return func(next dew.Middleware) dew.Middleware {
return dew.MiddlewareFunc(func(ctx dew.Context) error {
// Check if a transaction is already present in the context
if tx, ok := ctx.Context().Value("tx").(*sql.Tx); ok && tx != nil {
// Transaction already exists, proceed without creating a new one
return next.Handle(ctx)
}
// Start a new transaction
tx, err := db.BeginTx(ctx.Context(), nil)
if err != nil {
return fmt.Errorf("failed to begin transaction: %w", err)
}
// Attach the transaction to the context
txCtx := context.WithValue(ctx.Context(), "tx", tx)
ctx = ctx.WithContext(txCtx)
// Execute the command
err = next.Handle(ctx)
if err != nil {
// Roll back the transaction in case of an error
if rbErr := tx.Rollback(); rbErr != nil {
return fmt.Errorf("rollback failed: %w", rbErr)
}
return err
}
// Commit the transaction if everything went well
if commitErr := tx.Commit(); commitErr != nil {
return fmt.Errorf("commit failed: %w", commitErr)
}
return nil
})
}
}
func main() {
db, err := sql.Open("driver-name", "database-url")
if err != nil {
panic("failed to connect database")
}
defer db.Close()
bus := dew.New()
bus.UseDispatch(TransactionalMiddleware(db))
// Register your handlers and continue with application setup
}
This middleware example demonstrates how to:
- Start a new database transaction before executing a command.
- Attach the transaction to the context for use in handlers.
- Commit the transaction if the command executes successfully.
- Roll back the transaction if an error occurs during command execution.
This pattern is particularly useful for ensuring data consistency across multiple database operations within a single command.
Group handlers and apply middleware to a subset of handlers:
func main() {
bus := dew.New()
bus.Group(func(bus dew.Bus) {
bus.Use(dew.ACTION, middleware.Transaction)
bus.Use(dew.ALL, middleware.Logger)
bus.Register(new(UserProfileHandler))
bus.Group(func(g dew.Bus) {
bus.Use(dew.ACTION, middleware.Tracing)
bus.Register(new(SensitiveHandler))
})
})
}
Testing with Dew is straightforward. You can create mock handlers and use them in your tests. Here's an example:
package example_test
import (
"context"
"github.com/go-dew/dew"
"github.com/your/application/internal/action"
"testing"
)
func TestCreateUser(t *testing.T) {
// Create a new bus instance
bus := dew.New()
// Create a mock handler
mockHandler := &MockCreateUserHandler{
t: t,
expectedName: "John Doe",
expectedEmail: "john@example.com",
}
// Register the mock handler
bus.Register(mockHandler)
// Create a context with the bus
ctx := dew.NewContext(context.Background(), bus)
// Create the action
createUserAction := &action.CreateUserAction{
Name: "John Doe",
Email: "john@example.com",
}
// Dispatch the action
_, err := dew.Dispatch(ctx, createUserAction)
// Check for errors
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
// Check if the mock handler was called
if !mockHandler.called {
t.Fatalf("Expected mock handler to be called")
}
}
type MockCreateUserHandler struct {
t *testing.T
expectedName string
expectedEmail string
called bool
}
func (m *MockCreateUserHandler) HandleCreateUserAction(ctx context.Context, action *action.CreateUserAction) error {
m.called = true
if action.Name != m.expectedName {
m.t.Errorf("Expected name %s, got %s", m.expectedName, action.Name)
}
if action.Email != m.expectedEmail {
m.t.Errorf("Expected email %s, got %s", m.expectedEmail, action.Email)
}
return nil
}
This example demonstrates how to:
- Create a mock bus and handler for testing.
- Register the mock handler with the bus.
- Create and dispatch an action.
- Verify that the action was handled correctly by the mock handler.
You can extend this pattern to test various scenarios, including error cases and different types of actions and queries.
Results as of May 23, 2024 with Go 1.22.2 on darwin/arm64
BenchmarkMux/query-12 3012015 393.5 ns/op 168 B/op 7 allocs/op
BenchmarkMux/dispatch-12 2854291 419.1 ns/op 192 B/op 8 allocs/op
BenchmarkMux/query-with-middleware-12 2981778 407.8 ns/op 168 B/op 7 allocs/op
BenchmarkMux/dispatch-with-middleware-12 2699398 446.8 ns/op 192 B/op 8 allocs/op
We welcome contributions to Dew! Please see the contribution guide for more information.
Licensed under MIT License