README
¶
Hotel Booking
A hotel reservation and payment management system built with Go, demonstrating Domain-Driven Design (DDD) and Hexagonal Architecture (Ports & Adapters) patterns.
Table of Contents
- Overview
- Key Features
- Architecture
- Bounded Contexts
- Project Structure
- Getting Started
- Usage
- Testing
- Configuration
- Using as a Template
- Contributing
- License
Overview
This repository provides a reference implementation for structuring Go applications with clean architecture principles. It demonstrates how to:
- Organize code using Hexagonal Architecture (Ports & Adapters)
- Apply Domain-Driven Design tactical patterns (aggregates, entities, value objects, domain events)
- Structure code into Bounded Contexts with clear boundaries
- Implement Event-Driven Communication between contexts via Kafka
- Use the Saga Pattern for cross-context workflow orchestration
- Integrate authentication via OIDC/Keycloak
- Persist data with PostgreSQL using key/value storage (separate databases per bounded context)
Key Features
- Bounded Context Architecture — Separate reservation, payment, and orchestration contexts
- Developer Experience —
justtask runner, golangci-lint, comprehensive test coverage - Domain-Driven Design — Aggregates, entities, value objects, domain services, and domain events
- Event-Driven Communication — Kafka-based pub/sub for inter-context messaging
- Hexagonal Architecture — Clear separation between domain logic and infrastructure
- OIDC Authentication — Keycloak integration with session management
- PostgreSQL Persistence — Key/value storage with separate databases per bounded context
- Production-Ready Docker — Multi-stage build with PGO optimization
- Progressive Web App — Service worker, manifest, and offline support
- Saga Pattern — Event-driven booking workflow with compensation on failure
- MCP Integration — Model Context Protocol endpoint for AI tool integration
Architecture
┌─────────────────────────────────────────┐
│ Entry Point │
│ cmd/server/main.go │
│ (DI wiring, bootstrap) │
└─────────────────┬───────────────────────┘
│
┌────────────────────────────┼────────────────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌──────────────────┐
│ Inbound Adapter │ │ Domain Layer │ │Outbound Adapter │
│ (HTTP, Events) │─────────▶│ (Bounded Ctxs) │◀─────────│ (Repos, Gateways)│
│ │ │ │ │ │
│ implements │ │ defines │ │ implements │
│ domain ports │ │ ports │ │ domain ports │
└─────────────────┘ └─────────────────┘ └──────────────────┘
│
┌─────────────────┴──────────────┐
│ │
┌──────────┴──────────┐ │
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Reservation │ │ Payment │ │ Orchestration │
│ Context │ │ Context │ │ Layer │
│ │ │ │ │ │
│ aggregate.go │ │ aggregate.go │ │ booking_svc.go │
│ service.go │ │ service.go │ │ event_handlers │
│ events.go │ │ events.go │ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
└─────────────────────┴─────────────────────┘
│
┌──────────┴──────────┐
│ Shared Kernel │
│ (Money, IDs) │
└─────────────────────┘
Event-Driven Communication
Bounded contexts communicate via domain events through Kafka:
┌─────────────────┐ reservation.created ┌─────────────────┐
│ Reservation │ ─────────────────────────▶ │ Payment │
│ Context │ │ Context │
└─────────────────┘ └─────────────────┘
│
payment.authorized │
┌────────────────────────────────────────────────┘
│
▼
┌─────────────────┐ payment.captured ┌─────────────────┐
│ Orchestration │ ─────────────────────────▶ │ Reservation │
│ Layer │ │ Context │
└─────────────────┘ └─────────────────┘
Event Topics:
reservation.created— Payment context subscribes to authorize paymentreservation.confirmed— Notification context subscribesreservation.cancelled— Notification context subscribespayment.authorized— Orchestration subscribes to capture paymentpayment.captured— Reservation context subscribes to confirm reservationpayment.failed— Orchestration subscribes for compensation
Bounded Contexts
The domain is split into three bounded contexts with clear responsibilities:
| Context | Purpose | Key Aggregates | Database |
|---|---|---|---|
| Reservation | Room booking lifecycle | Reservation |
reservation_db |
| Payment | Payment processing | Payment |
payment_db |
| Orchestration | Cross-context coordination | Saga coordination | — |
Reservation Context
The Reservation aggregate manages the complete booking lifecycle:
Reservation (Aggregate Root)
├── ReservationID (Value Object)
├── GuestID (Value Object)
├── RoomID (Value Object)
├── DateRange (Value Object)
│ ├── CheckIn
│ └── CheckOut
├── TotalAmount (Money - Shared Kernel)
├── Guests (Entity Collection)
│ └── GuestInfo
│ ├── Name
│ ├── Email
│ └── PhoneNumber
└── ReservationStatus (Value Object)
States: pending → confirmed → active → completed
↘ cancelled
Business Rules:
- Minimum 1 night stay required
- Check-in must be in the future
- Cannot cancel within 24 hours of check-in
- Same-day checkout/check-in allowed (no overlap)
- Cancelled reservations don't block availability
Payment Context
The Payment aggregate handles payment processing with retry support:
Payment (Aggregate Root)
├── PaymentID (Value Object)
├── ReservationID (Shared Kernel)
├── Amount (Money - Shared Kernel)
├── PaymentMethod
├── TransactionID
├── PaymentStatus (Value Object)
│ States: pending → authorized → captured
│ ↘ failed ↘ refunded
└── Attempts (Entity Collection)
└── PaymentAttempt
├── Status
├── ErrorCode
└── AttemptedAt
Business Rules:
- Authorization-Capture pattern (Authorize → Capture)
- Failed payments can be retried
- Only captured payments can be refunded
Orchestration Layer (Saga Pattern)
Event-driven workflow coordination with compensation:
Booking Workflow:
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 1. Create │───▶│ 2. Authorize │───▶│ 3. Capture │
│ Reservation │ │ Payment │ │ Payment │
│ (pending) │ │ │ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │
▼ (on failure) ▼
Cancel Reservation Refund + Cancel
│
▼
┌─────────────────┐ ┌─────────────────┐
│ 5. Send │◀───│ 4. Confirm │
│ Notification │ │ Reservation │
└─────────────────┘ └─────────────────┘
Project Structure
hotel-booking/
├── .justfile # Task runner commands
├── cmd/server/ # HTTP server entry point
│ ├── main.go # DI wiring, bootstrap, lifecycle
│ └── assets/
│ ├── static/ # CSS, JS, images (embedded)
│ └── templates/ # HTML templates (*.tmpl, embedded)
│ └── error.tmpl # User-friendly error page
├── docker-compose.yml # Dev stack (PostgreSQL x2, Keycloak, Kafka, app)
├── Dockerfile # Multi-stage production build
├── migrations/
│ ├── reservation/
│ │ └── init.sql # Reservation database schema (key/value)
│ └── payment/
│ └── init.sql # Payment database schema (key/value)
├── internal/
│ ├── adapters/
│ │ ├── inbound/ # HTTP handlers, event subscribers
│ │ │ ├── router.go # HTTP routing & middleware
│ │ │ ├── http_{feature}.go # HTTP handlers
│ │ │ ├── http_error.go # Error page handler
│ │ │ └── event_subscriber.go
│ │ └── outbound/ # Repositories, gateways, publishers
│ │ ├── postgres_connection.go
│ │ ├── postgres_reservation_repository.go
│ │ ├── postgres_payment_repository.go
│ │ ├── repository_{checker}.go
│ │ ├── mock_{service}.go
│ │ └── event_publisher.go
│ └── domain/
│ ├── shared/ # Shared kernel
│ │ └── types.go # Cross-context types (Money, ReservationID)
│ ├── reservation/ # Reservation bounded context
│ │ ├── aggregate.go # Reservation aggregate + value objects
│ │ ├── entities.go # DateRange, GuestInfo
│ │ ├── events.go # Domain events
│ │ ├── ports.go # Interface definitions
│ │ ├── service.go # ReservationService
│ │ └── tools.go # MCP tools
│ ├── payment/ # Payment bounded context
│ │ ├── aggregate.go # Payment aggregate + status
│ │ ├── entities.go # PaymentAttempt
│ │ ├── events.go # Domain events
│ │ ├── ports.go # Interface definitions
│ │ ├── service.go # PaymentService
│ │ └── tools.go # MCP tools
│ └── orchestration/ # Cross-context coordination
│ ├── booking_service.go # Saga coordinator
│ ├── event_handlers.go # Event subscriptions
│ └── ports.go # NotificationService interface
└── docs/
└── ARCHITECTURE.md # Detailed architecture documentation
Getting Started
Prerequisites
- Docker and Docker Compose (or Podman)
- Go 1.24+
- golangci-lint (for linting/formatting)
- just task runner
Installation
-
Clone the repository:
git clone https://github.com/andygeiss/hotel-booking.git cd hotel-booking -
Install development tools:
just setupThis installs
docker-compose,golangci-lint,just, andpodmanvia Homebrew. -
Configure environment:
cp .env.example .env cp .keycloak.json.example .keycloak.json -
Start the development stack:
just upThis builds the Docker image and starts two PostgreSQL databases (reservation_db, payment_db), Keycloak, Kafka, and the application.
-
Access the application:
- App: http://localhost:8080/ui
- Keycloak Admin: http://localhost:8180/admin (admin:admin)
Usage
Commands
| Command | Description |
|---|---|
just build |
Build Docker image |
just down |
Stop all services |
just fmt |
Format code |
just lint |
Run linter |
just profile |
Generate CPU profile for PGO |
just setup |
Install development dependencies |
just test |
Run unit tests with coverage |
just test-integration |
Run integration tests |
just up |
Start full development stack |
Run Single Test
go test -v -run TestFunctionName ./internal/domain/reservation/...
Booking Workflow
Once the application is running:
- Login at http://localhost:8080/ui/login via Keycloak
- View Reservations at
/ui/reservationsto see your bookings - Create Reservation at
/ui/reservations/new:- Select a room and dates
- Total is calculated automatically (nights x room price)
- Submit to create a pending reservation
- View Details at
/ui/reservations/{id}to see reservation status - Cancel Reservation from the detail page (if >24 hours before check-in)
API Endpoints
| Endpoint | Method | Description |
|---|---|---|
/ui/ |
GET | Dashboard (authenticated) |
/ui/login |
GET | Login page |
/ui/reservations |
GET | List user's reservations |
/ui/reservations/new |
GET | Reservation form |
/ui/reservations |
POST | Create reservation |
/ui/reservations/{id} |
GET | Reservation detail |
/ui/reservations/{id}/cancel |
POST | Cancel reservation |
/ui/error |
GET | Error page (query params: title, message, details) |
/mcp |
POST | MCP JSON-RPC endpoint for AI tools |
MCP Endpoint
The application exposes an MCP (Model Context Protocol) endpoint for AI tool integration:
curl -X POST http://localhost:8080/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
See ARCHITECTURE.md for details on adding custom tools.
Testing
Unit Tests
Run all unit tests with coverage:
just test
This generates .coverage.pprof with coverage metrics.
Integration Tests
Integration tests require external services (PostgreSQL databases, Kafka, Keycloak):
just test-integration
Test Organization
- Unit tests are colocated with source files (
*_test.go) - Integration tests are tagged with
//go:build integration - Test fixtures live in
testdata/directories
Test Naming Convention
Tests follow the pattern: Test_{Component}_{Scenario}_Should_{ExpectedResult}
// Domain unit tests
func Test_Reservation_Confirm_From_Pending_Should_Change_Status(t *testing.T)
// Service tests
func Test_ReservationService_CreateReservation_Should_Succeed(t *testing.T)
// HTTP handler tests
func Test_Route_Liveness_Endpoint_Should_Return_200(t *testing.T)
Configuration
Configuration is managed via environment variables. Copy .env.example to .env and customize:
| Variable | Description | Default |
|---|---|---|
APP_NAME |
Display name for UI and PWA | Hotel Booking |
APP_DESCRIPTION |
Application description | Hotel reservation and payment management system |
APP_SHORTNAME |
Docker image/container name | hotel-booking |
KAFKA_BROKERS |
Kafka broker addresses | localhost:9092 |
OIDC_CLIENT_ID |
OIDC client ID | hotel-booking |
OIDC_CLIENT_SECRET |
OIDC client secret | Auto-generated |
OIDC_ISSUER |
Keycloak realm URL | http://localhost:8180/realms/local |
PORT |
HTTP server port | 8080 |
RESERVATION_DB_HOST |
Reservation database host | localhost |
RESERVATION_DB_PORT |
Reservation database port | 5432 |
RESERVATION_DB_USER |
Reservation database user | reservation |
RESERVATION_DB_PASSWORD |
Reservation database password | reservation_secret |
RESERVATION_DB_NAME |
Reservation database name | reservation_db |
RESERVATION_DB_SSLMODE |
SSL mode | disable |
PAYMENT_DB_HOST |
Payment database host | localhost |
PAYMENT_DB_PORT |
Payment database port | 5433 |
PAYMENT_DB_USER |
Payment database user | payment |
PAYMENT_DB_PASSWORD |
Payment database password | payment_secret |
PAYMENT_DB_NAME |
Payment database name | payment_db |
PAYMENT_DB_SSLMODE |
SSL mode | disable |
See .env.example for the complete list with documentation.
Using as a Template
Quick Start
-
Clone and reinitialize:
git clone https://github.com/andygeiss/hotel-booking my-project cd my-project rm -rf .git && git init -
Update module path:
go mod edit -module github.com/yourorg/my-project # Update import paths in all .go files -
Configure project identity:
cp .env.example .env # Edit APP_NAME, APP_SHORTNAME, APP_DESCRIPTION -
Add your domain logic:
- Create bounded contexts in
internal/domain/ - Add shared types to
internal/domain/shared/ - Implement adapters in
internal/adapters/ - Wire up in
cmd/server/main.go
- Create bounded contexts in
What to Keep
- Directory structure (
cmd/,internal/adapters/,internal/domain/) - Hexagonal architecture pattern
- Bounded context organization
- Event-driven communication pattern
cloud-native-utilsas infrastructure librarycontext.Contextthreading through all layers
What to Customize
- Bounded contexts (replace
reservation/,payment/,orchestration/with your domains) - Shared kernel types in
internal/domain/shared/ - Static assets and templates in
cmd/server/assets/ - PostgreSQL schemas in
migrations/(uses simple key/value pattern) - Environment configuration in
.env - Docker Compose services as needed
- Swap mock adapters for real implementations
Contributing
- Ensure all tests pass:
just test - Ensure code is formatted and linted:
just fmt && just lint - Follow hexagonal architecture patterns (ports in domain, adapters in adapters/)
- Maintain bounded context boundaries (communicate via events, not direct calls)
- Update documentation if architecture changes
License
This project is licensed under the MIT License. See LICENSE for details.
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
server
command
|
|
|
internal
|
|
|
domain/orchestration
Package orchestration contains the application services that coordinate workflows across bounded contexts.
|
Package orchestration contains the application services that coordinate workflows across bounded contexts. |
|
domain/payment
Package payment contains the Payment bounded context.
|
Package payment contains the Payment bounded context. |
|
domain/reservation
Package reservation contains the Reservation bounded context.
|
Package reservation contains the Reservation bounded context. |
|
domain/shared
Package shared contains types that are shared across bounded contexts.
|
Package shared contains types that are shared across bounded contexts. |