This repository is a production-minded Go boilerplate for a multi-channel bot platform using Hexagonal architecture.
It is designed to be used as a GitHub organization template, so defaults and structure prioritize reuse across teams.
It is not a WhatsApp-specific bot codebase. WhatsApp (aldinokemal/go-whatsapp-web-multidevice) is integrated as one replaceable transport gateway adapter.
internal/adapters/secondary/messaging/whatsapp/gowa is an anti-corruption boundary:
- GOWA DTOs stay local in
gowa/dto - mapping layer converts provider payloads to canonical
internal/transportcontracts - app/core logic never imports provider DTOs
This keeps transport integrations replaceable and allows later Discord/Slack adapters.
internal/core/domain: business entities/value objectsinternal/core/ports: foundational interfaces consumed by app/coreinternal/transport: canonical inbound/outbound contractsinternal/app/usecase: orchestration and execution logicinternal/app/workflow: stateful flow skeletonsinternal/app/policy: auth/rate-limit/feature flagsinternal/adapters/primary/http: webhook/API/health HTTP edgesinternal/adapters/secondary: messaging, queue, persistence, AI implementationsinternal/platform: config/bootstrap/db/cache/observability wiring
Dependency direction:
- core <- app <- adapters/platform/wiring
- provider-specific DTOs and HTTP payloads remain at adapter edge
This project is built as a modular monolith with strict ports-and-adapters boundaries:
- domain and ports define stable internal contracts
- use cases orchestrate behavior without provider coupling
- adapters translate external systems to canonical contracts
- platform/bootstrap wires dependencies without leaking infrastructure into business logic
flowchart LR
EXT[External Systems<br/>WhatsApp/GOWA, DB, Redis] --> ADP[Adapters]
ADP --> TRANS[Canonical Transport Contracts]
TRANS --> APP[App Use Cases + Policies + Workflows]
APP --> CORE[Core Domain + Ports]
CORE --> APP
APP --> ADP
ADP --> EXT
When adding a feature, move top-down by responsibility, not by provider:
- start from canonical contract or domain model
- add or update use case orchestration
- implement adapter translation and IO
- wire in bootstrap
- test mapper + use case + integration path
flowchart TD
A[Feature Request] --> B{Transport-facing?}
B -- Yes --> C[internal/transport]
B -- No --> D[internal/core/domain]
C --> E[internal/core/ports if needed]
D --> E
E --> F[internal/app/usecase]
F --> G[internal/adapters/secondary]
F --> H[internal/adapters/primary/http]
G --> I[internal/platform/bootstrap wiring]
H --> I
I --> J[test/contract + test/integration + usecase tests]
In isolation, this repo is one reusable bot execution unit. In a larger system, it acts as a channel-agnostic automation service behind API gateways, event buses, and shared data platforms.
flowchart LR
subgraph EnterpriseSystem["Big System"]
API[API Gateway / Internal Services]
BUS[Event Bus]
OBS[Central Observability]
DATA[Data Platform / BI]
end
subgraph BotTemplateRepo["This Project (Small Unit)"]
SERVER[bot-server]
WORKER[worker]
APPMOD[App Use Cases + Workflows]
ADAPTERS[Messaging/Queue/Persistence Adapters]
end
API --> SERVER
SERVER --> BUS
BUS --> WORKER
WORKER --> APPMOD
APPMOD --> ADAPTERS
ADAPTERS --> DATA
SERVER --> OBS
WORKER --> OBS
Inbound flow:
- HTTP webhook receives provider payload
- webhook handler transforms payload to canonical inbound event
- event enqueued to async queue
Worker flow:
- worker consumes queued canonical event
- app use case orchestrates policy + message handling
- optional AI hook can be used
- messenger port sends outbound message
- GOWA webhook payload mapped to canonical event
- canonical event enqueued in queue adapter (
asynqpackage) - worker ingests event via
ingest_event - message path parses command text
- if inbound text is
ping, worker repliespongthroughcore/ports.Messenger - GOWA sender adapter sends outbound payload
- User sends command prefix:
inv customer=<name> item=<name> qty=<int> price=<number> [currency=<code>] - Worker parses command and runs
generate_invoice_documentuse case - Use case builds a PDF invoice and stores it through
InvoiceRepository - Messenger sends the generated PDF back through GOWA
/send/fileendpoint
cmd/bot-server: thin HTTP ingress processcmd/worker: async business execution processinternal/transport/event: canonical inbound event modelinternal/transport/message: canonical outbound model + capability modeltest/contract: interface contract teststest/integration: integration placeholders and future E2E tests
- explicit constructors (
New...) - interfaces are explicit and minimal
- no junk-drawer
utils/helpers/servicespackages - app/core never depend on provider DTOs
- keep webhook layer thin and async-first
Compile-time import tests prevent layer violations:
go test ./internal/... -run "Test(Core|Transport|App|Adapters)DoesNot"Boundary rules:
- core: no imports from adapters, app, transport, or platform
- transport: no imports from adapters, app, or platform
- app: no imports from adapters or platform
- adapters: may import core and transport, but not app or platform
Violations fail at go test, not at code review.
- preserve architecture boundaries; avoid structural churn
- keep defaults generic, configurable, and provider-replaceable
- avoid machine-local or organization-private paths in committed docs/scripts
- treat new capabilities as modular additions, not repo-shape rewrites
Requirements:
- Go 1.22+
- Docker + Docker Compose
Commands:
make tidymake fmtmake testmake upmake down
Run server and worker locally without dockerized Go runtime:
make run-servermake run-worker
docker-compose.yml includes:
bot-serverworkergowaredispostgres
Environment knobs:
APP_DB_BACKEND=postgres|sqliteAPP_POSTGRES_DSN=...(postgres mode)APP_SQLITE_PATH=./data/bot.db(sqlite mode)APP_GOWA_SEND_MESSAGE_PATH=/send/messageAPP_GOWA_SEND_FILE_PATH=/send/file
- Add adapter under
internal/adapters/secondary/messaging/<provider> - keep provider DTOs local to adapter package
- implement
core/ports.Messenger - map provider inbound payload to canonical
transport/event - map canonical outbound message to provider wire DTO
- register provider-specific webhook route at HTTP edge
- keep provider DTOs at adapter edge
- keep canonical contracts in
internal/transport - keep business flow in worker by default
- avoid direct app/core imports of provider-specific packages
- real Postgres persistence implementations
- delivery status reconciliation worker path
- richer workflow engine and session lifecycle
- observability metrics/tracing and dashboards
- multi-tenant authorization and feature flags
- Discord/Slack transport adapters