Go DDD Modular Monolith with Hexagonal Architecture

A Golang example project demonstrating Domain-Driven Design (DDD) in a Modular Monolith architecture with Hexagonal (Ports and Adapters) pattern.
Read the original Blogpost here:
Please read the first article, then proceed to the second and this repository to understand the reasoning behind the current structure.
Quick Links:
Table of Contents
Architecture Overview
This project combines three powerful architectural patterns:
Modular Monolith
- Multiple independent modules (payment, payment-settings) in a single deployable unit
- Each module has clear boundaries and communicates via well-defined interfaces
- Modules can be developed, tested, and evolved independently
- All modules share the same process and database, simplifying deployment and transactions
Hexagonal Architecture (Ports & Adapters)
- Domain logic (hexagon core) is isolated from external concerns
- Inbound adapters: REST controllers, cron jobs (drive the application)
- Outbound adapters: Repositories, external APIs (driven by the application)
- Ports: Interfaces that define contracts between core and adapters
Domain-Driven Design (DDD)
- Clear separation between domain logic and infrastructure
- Each module represents a bounded context
- Domain entities and business rules are at the center
Project Structure
.
├── application/ # Application entry point
├── cmd/ # CLI commands (Cobra)
│ ├── rest.go # REST API server command
│ ├── cron_update_payment.go # Cron job command
│ └── root.go # Root command configuration
├── modules/ # Business modules (bounded contexts)
│ ├── payment/
│ │ ├── factory/ # Module factory for dependency injection
│ │ ├── internal/
│ │ │ ├── adapter/ # Inbound/Outbound adapters
│ │ │ │ ├── controller/ # REST controllers (inbound)
│ │ │ │ ├── cron/ # Cron jobs (inbound)
│ │ │ │ └── repository/ # Database repository (outbound)
│ │ │ ├── ports/ # Interface definitions
│ │ │ └── service/ # Business logic
│ │ ├── module.go # Module registration
│ │ └── payment.go # Domain entities / Public API for the domain/module
│ └── payment-settings/ # Similar structure
├── pkg/ # Shared utilities
│ ├── config/ # Configuration management
│ ├── dbutils/ # Database utilities
│ ├── errors/ # Error handling
│ ├── logger/ # Logging utilities
│ ├── middlewares/ # HTTP middlewares
│ └── uniqueid/ # ID generation (ULID)
├── migrations/ # Database migrations
└── docker-compose.yml # Docker setup
Features
- Clean Architecture with clear separation of concerns
- Hexagonal Architecture (Ports and Adapters)
- Domain-Driven Design principles
- RESTful API using Echo framework
- PostgreSQL database with migrations
- Structured logging with zerolog
- Comprehensive testing (unit and E2E)
- Docker support
- Hot reload development with Air
- CLI commands with Cobra
- Mock generation with Mockery
- Code linting with golangci-lint
Prerequisites
- Go 1.24.4 or higher
- Docker and Docker Compose (for database)
- Make (for running commands)
Quick Start
Option A: One-Command Setup (Recommended for First Time)
If you want everything set up automatically:
# Clone the repository
git clone https://github.com/bxcodec/golang-ddd-modular-monolith-with-hexagonal.git
cd golang-ddd-modular-monolith-with-hexagonal
# Copy environment file
cp .env.example .env
# Install dependencies and start everything
make init
make up
The make up command will:
- Start PostgreSQL in Docker
- Start the application with hot reload (you'll need to run migrations separately, see below)
After the app starts, open a new terminal and run migrations:
# In a new terminal window
cd golang-ddd-modular-monolith-with-hexagonal
make migrate-up
# Press Enter when prompted to apply all migrations
The API will be available at http://localhost:9090
curl http://localhost:9090/health
Expected response:
{
"status": "ok",
"mode": "rest",
"environment": "development"
}
CLI Commands
The application supports multiple commands via Cobra:
Start REST API Server
go run application/main.go rest
Run Cron Job
go run application/main.go cron-update-payment
Options:
--batch-size - Number of payments to process in one batch
--dry-run - Run in dry-run mode without making changes
Example:
go run application/main.go cron-update-payment --batch-size 100 --dry-run
Development
Hot Reload with Air
Air is automatically started when you run make up. It watches for file changes and rebuilds the application.
Running Tests
# Run unit tests
make test-unit
# Run E2E tests
make test-e2e
# Run all tests
make test-all
# Run tests with detailed output
make tests-complete
make fmt
This will format your code using:
Linting
make lint
Generate Mocks
make go-generate
This uses Mockery to generate mocks based on the .mockery.yml configuration.
Database Migrations
Apply Migrations
make migrate-up
Rollback Migrations
make migrate-down
Create New Migration
make migrate-create
Drop Database
make migrate-drop
Docker
Build Docker Image
make image-build
Run with Docker Compose
docker compose up
This will start both PostgreSQL and the application.
Makefile Commands
Run make help to see all available commands:
make help
Key commands:
make up - Start development environment
make down - Stop Docker containers
make destroy - Teardown everything (removes volumes)
make build - Build the binary
make tests - Run tests
make lint - Run linter
make fmt - Format code
make clean - Clean artifacts
Testing
The project includes both unit tests and E2E tests:
Unit Tests
Located alongside the code, testing individual components in isolation using mocks.
E2E Tests
Use testcontainers to spin up real PostgreSQL instances for integration testing.
Test Coverage
make test-all
Coverage reports are generated in coverage-all.out.
Architecture Benefits
Why Modular Monolith?
- Simpler than microservices while maintaining modularity
- Easy refactoring to microservices if needed (modules are already isolated)
- Shared infrastructure reduces operational complexity
- No network latency between modules
Why Hexagonal Architecture?
- Business logic is independent of frameworks and tools
- Easy to swap implementations (e.g., change database, HTTP framework)
- Highly testable (mock adapters, test ports)
- Clear separation between what the application does and how it does it
Why DDD?
- Focus on core domain and domain logic
- Ubiquitous language between developers and domain experts
- Clear bounded contexts
- Better code organization and maintainability
Module Communication
Modules communicate via well-defined ports (interfaces):
// payment module depends on payment-settings module
type IPaymentSettingsPort interface {
GetPaymentSetting(id string) (PaymentSetting, error)
// ...
}
// payment module uses the interface, not the concrete implementation
paymentModule := paymentfactory.NewModule(paymentfactory.ModuleConfig{
DB: db,
PaymentSettingsPort: paymentSettingsModule.Service, // dependency injection
})
This allows:
- Clear contracts between modules
- Easy testing with mocks
- Potential extraction to microservices
Production Deployment
Build Binary
make build
The binary will be created as engine in the project root.
Run Binary
./engine rest
Docker Deployment
docker build -t payment-app:latest .
docker run -p 9090:9090 \
-e POSTGRES_HOST=your-db-host \
-e POSTGRES_PASSWORD=your-db-password \
payment-app:latest rest
Contributing
Contributions are welcome. Please follow these guidelines:
- Fork the repository
- Create a feature branch
- Write tests for new features
- Ensure all tests pass
- Run linter and formatter
- Submit a pull request
License
This project is licensed under the MIT License - see the LICENSE file for details.
Author
Iman Tumorang
Acknowledgments
This project is inspired by various clean architecture implementations and aims to demonstrate a practical approach to building maintainable Go applications.