A Golang example project demonstrating Domain-Driven Design (DDD) in a Modular Monolith architecture with Hexagonal (Ports and Adapters) pattern.
- Combining Modular Monolith and Hexagonal Architecture while Maintaining Domain Driven Design Principles (part 1)
- Developing Modular Monolith and Hexagonal Architecture in Golang while Maintaining Domain Driven Design Principles (part 2)
Please read the first article, then proceed to the second and this repository to understand the reasoning behind the current structure.
Quick Links:
- Contributing Guide - How to contribute
- Postman Collection - API documentation
- Architecture Overview
- Project Structure
- Features
- Prerequisites
- Quick Start
- CLI Commands
- Development
- Database Migrations
- Docker
- Testing
- Architecture Benefits
- Production Deployment
- Contributing
- License
This project combines three powerful architectural patterns:
- 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
- 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
- Clear separation between domain logic and infrastructure
- Each module represents a bounded context
- Domain entities and business rules are at the center
.
├── 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
- 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
- Go 1.24.4 or higher
- Docker and Docker Compose (for database)
- Make (for running commands)
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 upThe 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 migrationsThe API will be available at http://localhost:9090
curl http://localhost:9090/healthExpected response:
{
"status": "ok",
"mode": "rest",
"environment": "development"
}The application supports multiple commands via Cobra:
go run application/main.go restgo run application/main.go cron-update-paymentOptions:
--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-runAir is automatically started when you run make up. It watches for file changes and rebuilds the application.
# 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-completemake fmtThis will format your code using:
- gofmt
- gofumpt
- goimports
make lintmake go-generateThis uses Mockery to generate mocks based on the .mockery.yml configuration.
make migrate-upmake migrate-downmake migrate-createmake migrate-dropmake image-builddocker compose upThis will start both PostgreSQL and the application.
Run make help to see all available commands:
make helpKey commands:
make up- Start development environmentmake down- Stop Docker containersmake destroy- Teardown everything (removes volumes)make build- Build the binarymake tests- Run testsmake lint- Run lintermake fmt- Format codemake clean- Clean artifacts
The project includes both unit tests and E2E tests:
Located alongside the code, testing individual components in isolation using mocks.
Use testcontainers to spin up real PostgreSQL instances for integration testing.
make test-allCoverage reports are generated in coverage-all.out.
- 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
- 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
- Focus on core domain and domain logic
- Ubiquitous language between developers and domain experts
- Clear bounded contexts
- Better code organization and maintainability
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
make buildThe binary will be created as engine in the project root.
./engine restdocker 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 restContributions 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
This project is licensed under the MIT License - see the LICENSE file for details.
- Clean Architecture: https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
- Hexagonal Architecture: https://alistair.cockburn.us/hexagonal-architecture/
- Hexagonal Architecture Example: https://github.com/jmgarridopaz/bluezone
- Presentation Domain Data Layering: https://martinfowler.com/bliki/PresentationDomainDataLayering.html
- Original Clean Architecture Example: https://github.com/bxcodec/go-clean-arch
- Modular Monolith in .NET: https://github.com/kgrzybek/modular-monolith-with-ddd
- Architecting Robust .NET: https://medium.com/@mail2mhossain/architecting-robust-net-dfa4f3725142
Iman Tumorang
This project is inspired by various clean architecture implementations and aims to demonstrate a practical approach to building maintainable Go applications.