English | 日本語
NOTE: This repository is an example to demonstrate "how to implement DDD architecture in a Go application." If you use this as a reference, ensure to implement authentication and security before deploying it to a real-world environment!
- Python implementation: dddpy
- gRPC & Connect-Go: Modern RPC framework with HTTP/2 support
- Ent: Type-safe ORM for Go with code generation
- SQLite: Lightweight database for development
- Buf: Protocol buffer management and code generation
- Samber/do: Dependency injection container
- Atlas: Database migration tool
- Install dependencies:
make install
- Generate code (protobuf, ent, mocks):
make buf-generate
make ent-generate
make mockgen
- Run database migrations:
make migrate-up
- Start the gRPC server:
make server
The server will start on port 8080 by default. You can override this by setting the PORT
environment variable.
The directory structure is based on Onion Architecture:
internal/
├── domain/ # Domain Layer (Entities, Value Objects, Repository Interfaces)
│ └── todo/
├── application/ # Application Layer (Use Cases)
│ ├── todoapp/
│ └── uow/ # Unit of Work pattern
├── infrastructure/ # Infrastructure Layer (Repository Implementations, External Services)
│ ├── ent/ # Ent ORM (Schema, Generated Code, Repository)
│ ├── sqlite/ # Database migrations
│ └── di/ # Dependency injection setup
└── api/ # Presentation Layer (gRPC Handlers, Generated Code)
└── grpc/ # gRPC handlers and generated protobuf code
The domain layer contains the core business logic and is independent of external concerns. It includes:
- Entities: Core business objects with identity
- Value Objects: Immutable objects that describe characteristics
- Repository Interfaces: Contracts for data persistence
- Domain Services: Business logic that doesn't belong to a single entity
The Todo
entity represents the core business object:
// Todo is the entity that represents a todo item.
type Todo struct {
id TodoID
title string
body string
status TodoStatus
createdAt time.Time
updatedAt time.Time
completedAt *time.Time
}
// NewTodo creates a new Todo.
func NewTodo(title string, body string) (*Todo, error) {
now := time.Now()
return &Todo{
id: NewTodoID(),
title: title,
body: body,
status: TodoStatusNotStarted,
createdAt: now,
updatedAt: now,
completedAt: nil,
}, nil
}
Key characteristics of the entity:
- Encapsulates business rules and invariants
- Provides methods for state transitions (
Start()
,Complete()
) - Maintains data integrity through validation
- Uses value objects for type safety (
TodoID
,TodoStatus
)
Value objects ensure type safety and encapsulate validation logic:
// TodoID represents a unique identifier for a Todo.
type TodoID uuid.UUID
// NewTodoID creates a new TodoID.
func NewTodoID() TodoID {
return TodoID(uuid.New())
}
// TodoStatus represents the status of a Todo.
type TodoStatus string
const (
TodoStatusNotStarted TodoStatus = "not_started"
TodoStatusInProgress TodoStatus = "in_progress"
TodoStatusCompleted TodoStatus = "completed"
)
The repository interface defines the contract for data persistence without specifying implementation details:
// TodoRepository is the interface that wraps the basic CRUD operations for Todo.
type TodoRepository interface {
Create(ctx context.Context, todo *Todo) error
Update(ctx context.Context, todo *Todo) error
FindAll(ctx context.Context) ([]*Todo, error)
FindByID(ctx context.Context, id TodoID) (*Todo, error)
Delete(ctx context.Context, id TodoID) error
}
The infrastructure layer contains implementations of interfaces defined in the domain layer. It includes:
- Repository Implementations: Concrete implementations using Ent ORM
- Database Schema: Ent schema definitions
- External Service Integrations: HTTP clients, third-party APIs, etc.
- Dependency Injection: Service container configuration
The todoRepository
implements the domain repository interface using Ent ORM:
// todoRepository is the implementation of the TodoRepository interface.
type todoRepository struct{}
// Create creates the Todo.
func (r todoRepository) Create(ctx context.Context, todo *todo.Todo) error {
tx, err := db.GetTx(ctx)
if err != nil {
return err
}
status := todoschema.Status(todo.Status().String())
_, err = tx.TodoSchema.Create().
SetTitle(todo.Title()).
SetBody(todo.Body()).
SetStatus(status).
Save(ctx)
if err != nil {
return fmt.Errorf("failed to create todo: %w", err)
}
return nil
}
Unlike the repository interface, the implementation code in the infrastructure layer can contain details specific to a particular technology (Ent ORM and SQLite in this example).
The infrastructure layer handles conversion between domain entities and database models:
// convertEntToTodo converts ent.TodoSchema to domain Todo
func convertEntToTodo(v *entgen.TodoSchema) (*todo.Todo, error) {
status, err := todo.NewTodoStatusFromString(string(v.Status))
if err != nil {
return nil, fmt.Errorf("failed to convert status %v: %w", v.Status, err)
}
return todo.ReconstructTodo(v.ID, v.Title, *v.Body, status, v.CreatedAt, v.UpdatedAt), nil
}
The application layer contains the application-specific business rules. It includes:
- Use Case implementations: Application services that orchestrate domain objects
- Transaction management: Unit of Work pattern for data consistency
- Error handling: Application-specific error handling
Each use case is implemented as a separate struct with a single Execute
method:
// CreateTodoUseCase is the interface that wraps the basic CreateTodo operation.
type CreateTodoUseCase interface {
Execute(ctx context.Context, req CreateTodoRequest) error
}
// createTodoUseCase is the implementation of the CreateTodoUseCase interface.
type createTodoUseCase struct {
todoRepository todo.TodoRepository
txRunner uow.TransactionRunner
}
// Execute creates a new Todo.
func (u createTodoUseCase) Execute(ctx context.Context, req CreateTodoRequest) error {
todo, err := todo.NewTodo(req.Title, req.Body)
if err != nil {
return fmt.Errorf("failed to create todo: %w", err)
}
err = u.txRunner.RunInTx(ctx, func(ctx context.Context) error {
if err := u.todoRepository.Create(ctx, todo); err != nil {
return fmt.Errorf("failed to save todo: %w", err)
}
return nil
})
if err != nil {
return fmt.Errorf("failed to execute transaction: %w", err)
}
return nil
}
Key characteristics of use cases:
- Single responsibility principle
- Transaction management through Unit of Work pattern
- Clear interface definition
- Dependency injection through constructor
The application layer uses the Unit of Work pattern to ensure data consistency:
// TransactionRunner provides transaction management capabilities.
type TransactionRunner interface {
RunInTx(ctx context.Context, fn func(ctx context.Context) error) error
}
The presentation layer handles gRPC requests and responses. It includes:
- gRPC Handlers: Convert between protobuf messages and domain objects
- Generated Code: Protocol buffer generated code and Connect-Go handlers
- Input Validation: Request validation using buf validate
- Error Handling: Convert domain errors to gRPC status codes
- Middleware: Cross-cutting concerns like logging and error handling
The handlers are organized under the api/grpc
directory:
func (h *todoHandler) CreateTodo(
ctx context.Context,
req *connect.Request[v1.CreateTodoRequest],
) (*connect.Response[v1.CreateTodoResponse], error) {
// Extract body value if present
body := ""
if req.Msg.Body != nil {
body = *req.Msg.Body
}
// Create use case request
useCaseReq := todoapp.CreateTodoRequest{
Title: req.Msg.Title,
Body: body,
}
// Execute use case
if err := h.createTodoUseCase.Execute(ctx, useCaseReq); err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
// Return response
return connect.NewResponse(&v1.CreateTodoResponse{}), nil
}
The API contract is defined using Protocol Buffers with validation:
// TodoService provides all todo-related operations
service TodoService {
// CreateTodo creates a new todo item
rpc CreateTodo(CreateTodoRequest) returns (CreateTodoResponse);
// GetTodo retrieves a todo item by its ID
rpc GetTodo(GetTodoRequest) returns (GetTodoResponse);
// GetTodos retrieves all todo items
rpc GetTodos(GetTodosRequest) returns (GetTodosResponse);
// UpdateTodo updates an existing todo item
rpc UpdateTodo(UpdateTodoRequest) returns (UpdateTodoResponse);
// StartTodo changes the todo status to in progress
rpc StartTodo(StartTodoRequest) returns (StartTodoResponse);
// CompleteTodo changes the todo status to completed
rpc CompleteTodo(CompleteTodoRequest) returns (CompleteTodoResponse);
// DeleteTodo deletes a todo item
rpc DeleteTodo(DeleteTodoRequest) returns (DeleteTodoResponse);
}
message CreateTodoRequest {
string title = 1 [(buf.validate.field).string.min_len = 1];
optional string body = 2;
}
- Clone this repository
- Install dependencies:
make install
- Generate code:
make buf-generate && make ent-generate
- Run migrations:
make migrate-up
- Start the server:
make server
- The gRPC server will be available at
localhost:8080
- Create a new todo:
grpcurl -plaintext -d '{
"title": "Implement DDD architecture",
"body": "Create a sample application using DDD principles"
}' localhost:8080 oniongo.v1.TodoService/CreateTodo
- Get all todos:
grpcurl -plaintext -d '{}' localhost:8080 oniongo.v1.TodoService/GetTodos
- Get a specific todo:
grpcurl -plaintext -d '{
"id": "550e8400-e29b-41d4-a716-446655440000"
}' localhost:8080 oniongo.v1.TodoService/GetTodo
- Start a todo:
grpcurl -plaintext -d '{
"id": "550e8400-e29b-41d4-a716-446655440000"
}' localhost:8080 oniongo.v1.TodoService/StartTodo
- Complete a todo:
grpcurl -plaintext -d '{
"id": "550e8400-e29b-41d4-a716-446655440000"
}' localhost:8080 oniongo.v1.TodoService/CompleteTodo
This project uses several code generation tools:
# Generate protobuf code
make buf-generate
# Generate Ent ORM code
make ent-generate
# Generate mocks for testing
make mockgen
# Create a new migration
make migrate-diff name=add_new_field
# Apply migrations
make migrate-up
Run all unit tests with coverage:
make test
Or run tests for a specific package:
go test -v ./internal/domain/todo/...
go test -v ./internal/application/todoapp/...
This project uses runn for API end-to-end testing. runn allows you to write test scenarios in YAML format and execute them against the running server.
First, ensure the server is running:
make server
Then run the e2e tests:
# Run all e2e tests
make e2e-test
# Run with verbose output
make e2e-test-verbose
The e2e test files are located in the e2e/
directory:
create_todo.yaml
: Tests todo creationget_todos.yaml
: Tests retrieving all todostodo_lifecycle.yaml
: Tests complete todo lifecycle (create, start, update, complete, delete)validation_test.yaml
: Tests API validation and error handling
Example e2e test scenario:
desc: Create a new todo
runners:
req: http://localhost:8080
steps:
create_todo:
desc: Create a new todo item
req:
/oniongo.v1.TodoService/CreateTodo:
post:
headers:
Content-Type: application/json
body:
application/json:
title: "Buy groceries"
body: "Milk, eggs, bread"
This project uses several tools to maintain code quality:
- golangci-lint: Comprehensive linter for Go
- buf: Protocol buffer linting and breaking change detection
- mockery: Mock generation for testing
# Format code
make fmt
# Run linter
make lint
The project uses the samber/do
library for dependency injection, ensuring loose coupling between layers:
// Dependency injection setup
func DependencyInjection() *do.Injector {
injector := do.New()
// Register dependencies
do.Provide(injector, todorepo.NewTodoRepository)
do.Provide(injector, todoapp.NewCreateTodoUseCase)
do.Provide(injector, todohandler.NewTodoServiceHandler)
return injector
}
Transaction management is handled through the Unit of Work pattern, ensuring data consistency across multiple operations:
err = u.txRunner.RunInTx(ctx, func(ctx context.Context) error {
// Multiple repository operations within a single transaction
return nil
})
The repository pattern abstracts data access logic, making the domain layer independent of specific database technologies:
// Domain layer defines the interface
type TodoRepository interface {
Create(ctx context.Context, todo *Todo) error
// ... other methods
}
// Infrastructure layer provides the implementation
type todoRepository struct{}
This project is licensed under the MIT License - see the LICENSE file for details.
This repository demonstrates how to implement Domain-Driven Design (DDD) and Onion Architecture in Go, providing a clean, maintainable, and testable codebase structure for building scalable applications.