Welcome to visit my blog post
This project is a Go microservice framework based on Hexagonal Architecture and Domain-Driven Design. It provides a clear project structure and design patterns to help developers build maintainable, testable, and scalable applications.
Hexagonal Architecture (also known as Ports and Adapters Architecture) divides the application into internal and external parts, implementing Separation of Concerns and Dependency Inversion Principle through well-defined interfaces (ports) and implementations (adapters). This architecture decouples business logic from technical implementation details, facilitating unit testing and feature extension.
- Domain-Driven Design (DDD) - Organize business logic through concepts like Aggregates, Entities, and Value Objects
- Hexagonal Architecture - Divide the application into domain, application, and adapter layers
- Dependency Injection - Use Wire for dependency injection, improving code testability and flexibility
- Repository Pattern - Abstract data access layer with transaction support
- Domain Events - Implement Event-Driven Architecture, supporting loosely coupled communication between system components
- CQRS Pattern - Command and Query Responsibility Segregation, optimizing read and write operations
- RESTful API - Implement HTTP API using the Gin framework
- Database Support - Integrate GORM with support for MySQL, PostgreSQL, and other databases
- Cache Support - Integrate Redis caching
- Logging System - Use Zap for high-performance logging
- Configuration Management - Use Viper for flexible configuration management
- Graceful Shutdown - Support graceful service startup and shutdown
- Unit Testing - Use go-sqlmock and redismock for database and cache mocking
- NoopTransaction - Provide no-operation transaction implementation, simplifying service layer interaction with repository layer
- Code Quality - Integrate Golangci-lint for code quality checks
- Commit Standards - Use Commitlint to ensure Git commit messages follow conventions
- Pre-commit Hooks - Use Pre-commit for code checking and formatting
- CI/CD - Integrate GitHub Actions for continuous integration and deployment
.
├── adapter/ # Adapter Layer - Interaction with external systems
│ ├── amqp/ # Message queue adapters
│ ├── dependency/ # Dependency injection configuration
│ ├── job/ # Scheduled task adapters
│ └── repository/ # Data repository adapters
│ ├── mysql/ # MySQL implementation
│ │ └── entity/ # Database entities
│ ├── postgre/ # PostgreSQL implementation
│ └── redis/ # Redis implementation
├── api/ # API Layer - Handle HTTP requests and responses
│ ├── dto/ # Data Transfer Objects
│ ├── error_code/ # Error code definitions
│ ├── grpc/ # gRPC API
│ └── http/ # HTTP API
│ ├── handle/ # Request handlers
│ ├── middleware/ # Middleware
│ ├── paginate/ # Pagination handling
│ └── validator/ # Request validation
├── application/ # Application Layer - Coordinate domain objects for use cases
│ ├── core/ # Core interfaces and error definitions
│ └── example/ # Example use case implementations
├── cmd/ # Command-line entry points
│ └── http_server/ # HTTP server startup
├── config/ # Configuration files and management
├── domain/ # Domain Layer - Core business logic
│ ├── aggregate/ # Aggregates
│ ├── event/ # Domain events
│ ├── model/ # Domain models
│ ├── repo/ # Repository interfaces
│ ├── service/ # Domain services
│ └── vo/ # Value objects
├── tests/ # Integration tests
└── util/ # Utility functions
├── clean_arch/ # Architecture checking tools
└── log/ # Logging utilities
The domain layer is the core of the application, containing business logic and rules. It is independent of other layers and does not depend on any external components.
-
Models: Domain entities and value objects
Example
: Example entity, containing basic properties like ID, name, alias, etc.
-
Repository Interfaces: Define data access interfaces
IExampleRepo
: Example repository interface, defining operations like create, read, update, delete, etc.IExampleCacheRepo
: Example cache interface, defining health check methodsTransaction
: Transaction interface, supporting transaction begin, commit, and rollback
-
Domain Services: Handle business logic across entities
ExampleService
: Example service, handling business logic for example entities, interacting with repositories and event bus
-
Domain Events: Define events within the domain
ExampleCreatedEvent
: Example creation eventExampleUpdatedEvent
: Example update eventExampleDeletedEvent
: Example deletion event
The application layer coordinates domain objects to complete specific application tasks. It depends on the domain layer but does not contain business rules.
-
Use Cases: Define application functionality
CreateExampleUseCase
: Create example use caseGetExampleUseCase
: Get example use caseUpdateExampleUseCase
: Update example use caseDeleteExampleUseCase
: Delete example use caseFindExampleByNameUseCase
: Find example by name use case
-
Commands and Queries: Implement CQRS pattern
- Each use case defines Input and Output structures, representing command/query inputs and results
-
Event Handlers: Process domain events
LoggingEventHandler
: Logging event handler, recording all eventsExampleEventHandler
: Example event handler, processing events related to examples
The adapter layer implements interaction with external systems, such as databases and message queues.
-
Repository Implementation: Implement data access interfaces
EntityExample
: MySQL implementation of example repositoryNoopTransaction
: No-operation transaction implementation, simplifying testingMySQL
: MySQL connection and transaction managementRedis
: Redis connection and basic operations
-
Message Queue Adapters: Implement message publishing and subscription
- Support for Kafka and other message queue integrations
-
Scheduled Tasks: Implement scheduled tasks
- Cron-based task scheduling system
The API layer handles HTTP requests and responses, serving as the entry point to the application.
-
Controllers: Handle HTTP requests
CreateExample
: Create example APIGetExample
: Get example APIUpdateExample
: Update example APIDeleteExample
: Delete example APIFindExampleByName
: Find example by name API
-
Middleware: Implement cross-cutting concerns
- Internationalization support
- CORS support
- Request ID tracking
- Request logging
-
Data Transfer Objects (DTOs): Define request and response data structures
CreateExampleReq
: Create example requestUpdateExampleReq
: Update example requestDeleteExampleReq
: Delete example requestGetExampleReq
: Get example request
This project uses Google Wire for dependency injection, organizing dependencies as follows:
// Initialize services
func InitializeServices(ctx context.Context) (*service.Services, error) {
wire.Build(
// Repository dependencies
entity.NewExample,
wire.Bind(new(repo.IExampleRepo), new(*entity.EntityExample)),
// Event bus dependencies
provideEventBus,
wire.Bind(new(event.EventBus), new(*event.InMemoryEventBus)),
// Service dependencies
provideExampleService,
provideServices,
)
return nil, nil
}
// Provide event bus
func provideEventBus() *event.InMemoryEventBus {
eventBus := event.NewInMemoryEventBus()
// Register event handlers
loggingHandler := event.NewLoggingEventHandler()
exampleHandler := event.NewExampleEventHandler()
eventBus.Subscribe(loggingHandler)
eventBus.Subscribe(exampleHandler)
return eventBus
}
// Provide example service
func provideExampleService(repo repo.IExampleRepo, eventBus event.EventBus) *service.ExampleService {
exampleService := service.NewExampleService(repo)
exampleService.EventBus = eventBus
return exampleService
}
// Provide services container
func provideServices(exampleService *service.ExampleService, eventBus event.EventBus) *service.Services {
return service.NewServices(exampleService, eventBus)
}
Domain events are used for communication between system components, implementing a loosely coupled event-driven architecture:
// Publish event
evt := event.NewExampleCreatedEvent(example.Id, example.Name, example.Alias)
e.EventBus.Publish(ctx, evt)
// Handle event
func (h *ExampleEventHandler) HandleEvent(ctx context.Context, event Event) error {
switch event.EventName() {
case ExampleCreatedEventName:
return h.handleExampleCreated(ctx, event)
// ...
}
return nil
}
Application layer use cases implement the Command and Query Responsibility Segregation (CQRS) pattern:
// Create example use case
func (h *CreateExampleHandler) Handle(ctx context.Context, input interface{}) (interface{}, error) {
createInput, ok := input.(CreateExampleInput)
if !ok {
return nil, core.ErrInvalidInput
}
example := &model.Example{
Name: createInput.Name,
Alias: createInput.Alias,
}
createdExample, err := h.ExampleService.Create(ctx, example)
if err != nil {
return nil, err
}
return CreateExampleOutput{
ID: createdExample.Id,
Name: createdExample.Name,
Alias: createdExample.Alias,
}, nil
}
This project follows unified coding standards to ensure code quality and consistency. For detailed guidelines, please refer to CODING_STYLE.md.
Key standards include:
- Code format and style (using go fmt and golangci-lint)
- Naming conventions (package names, variable names, interfaces and structs)
- Import package ordering
- Comment standards
- Error handling standards (using util/errors package)
- Testing standards
- CI/CD standards
Developers should ensure compliance with these standards before submitting code. Use the following commands for verification:
# Format code
make fmt
# Code quality check
make lint
# Run tests
make test
This project implements transaction interfaces and no-operation transactions, supporting different transaction management strategies:
// Transaction interface
type Transaction interface {
Begin() error
Commit() error
Rollback() error
Conn(ctx context.Context) interface{}
}
// No-operation transaction implementation
type NoopTransaction struct {
conn interface{}
}
// Using transactions in services
func (s *ExampleService) Create(ctx context.Context, example *model.Example) (*model.Example, error) {
// Create a no-operation transaction
tr := repo.NewNoopTransaction(s.Repository)
createdExample, err := s.Repository.Create(ctx, tr, example)
// ...
}
This project implements clear data mapping and transformation between different layers:
// Entity to model conversion
func (e EntityExample) ToModel() *model.Example {
return &model.Example{
Id: e.ID,
Name: e.Name,
Alias: e.Alias,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
}
}
// Model to entity conversion
func (e *EntityExample) FromModel(m *model.Example) {
e.ID = m.Id
e.Name = m.Name
e.Alias = m.Alias
e.CreatedAt = m.CreatedAt
e.UpdatedAt = m.UpdatedAt
}
The project has recently undergone the following improvements:
- Problem: The project had both v1 and v2 API versions, causing code duplication and maintenance difficulties
- Solution:
- Unified API routes, placing all APIs under the
/api
path - Retained the
/v2
path for backward compatibility - Used application layer use cases to handle all requests, phasing out direct domain service calls
- Unified API routes, placing all APIs under the
- Problem: Wire dependency injection configuration had duplicate binding issues, causing generation failures
- Solution:
- Refactored the
wire.go
file, removing duplicate binding definitions - Used provider functions instead of direct bindings
- Added event handler registration logic
- Refactored the
- Problem: The project used global variables to store service instances, violating dependency injection principles
- Solution:
- Removed the use of global variables
service.ExampleSvc
andservice.EventBus
- Passed service instances through dependency injection
- Initialized services using dependency injection when starting the HTTP server
- Removed the use of global variables
- Problem: Application layer use cases were not fully utilized, with the HTTP server not enabling the application layer by default
- Solution:
- Enabled application layer use cases by default
- Used the use case factory to create and manage use cases
- Implemented clearer error handling and response mapping
The project has recently undergone the following optimizations:
-
Environment Variable Support:
- Added functionality for environment variable overrides for configuration files, making the application more flexible in containerized deployments
- Used a unified prefix (APP_) and hierarchical structure (e.g., APP_MYSQL_HOST) to organize environment variables
-
Unified Error Handling:
- Implemented an application-level error type system, supporting different types of errors (validation, not found, unauthorized, etc.)
- Added unified error response handling, mapping internal errors to appropriate HTTP status codes
- Improved error logging to ensure all unexpected errors are properly recorded
-
Request Logging Middleware:
- Added detailed request logging middleware to record request methods, paths, status codes, latency, and other information
- In debug mode, request and response bodies can be logged to help developers troubleshoot issues
- Intelligently identifies content types to avoid logging binary content
-
Request ID Tracking:
- Generated unique request IDs for each request, facilitating tracking in distributed systems
- Returned request IDs in response headers for client reference
- Included request IDs in logs to correlate multiple log entries for the same request
-
Graceful Shutdown:
- Implemented a graceful shutdown mechanism for the server, ensuring all in-flight requests are completed before shutting down
- Added shutdown timeout settings to prevent the shutdown process from hanging indefinitely
- Improved signal handling, supporting SIGINT and SIGTERM signals
-
Internationalization Support:
- Added translation middleware for multi-language validation error messages
- Automatically selected appropriate language based on the Accept-Language header
-
CORS Support:
- Added CORS middleware to handle cross-origin requests
- Configured allowed origins, methods, headers, and credentials
-
Debugging Tools:
- Integrated pprof performance analysis tools for diagnosing performance issues in production environments
- Can be enabled or disabled via configuration file
These optimizations make the project more robust, maintainable, and provide a better development experience.
Start MySQL using Docker:
docker run --name mysql-local \
-e MYSQL_ROOT_PASSWORD=mysqlroot \
-e MYSQL_DATABASE=go-hexagonal \
-e MYSQL_USER=user \
-e MYSQL_PASSWORD=mysqlroot \
-p 3306:3306 \
-d mysql:latest
# Install development tools
make init && make precommit.rehook
Or install manually:
# Install pre-commit
brew install pre-commit
# Install golangci-lint
brew install golangci-lint
# Install commitlint
npm install -g @commitlint/cli @commitlint/config-conventional
# Add commitlint configuration
echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js
# Add pre-commit hook
make precommit.rehook
# Run the project
go run cmd/main.go
# Run tests
go test ./...
- Dependency Injection Improvements - Enhance Wire dependency injection configuration
- HTTP Handling Improvements - Optimize HTTP request handling implementation
- Domain Event Enhancements - Improve domain event mechanisms
- gRPC Support - Add gRPC service implementation
- Hot Reload Configuration - Implement configuration hot reloading
- Monitoring Integration - Integrate Prometheus monitoring
- Message Queue Integration - Integrate Kafka and other message queues
- Architecture
- Project Standards
- Code References