This repository exists to practice hexagonal architecture and experiment with design/testing patterns in a small, safe codebase.
- Learn by doing, not by building production infrastructure.
- Keep the domain tiny (vehicle telemetry) so architecture decisions stay visible.
- Prefer unit tests and clear boundaries over framework-heavy setup.
- Domain:
internal/domain - Application services:
internal/application - Ports:
internal/ports - Adapters:
internal/adapters/inbound|outbound|shared - Composition root:
cmd/telemetry-playground/main.go
Dependency direction is intentional:
domainhas no adapter dependencies.portsdefine interfaces used by application services.- inbound/outbound adapters depend on ports.
cmd/.../main.gowires concrete implementations.
domaincontains domain structs and domain-level logic only.portscontains interfaces (input/output contracts).
Why: keeps the business core framework-agnostic and easy to unit test.
The service forwards telemetry northbound instead of persisting/querying it.
- Outbound port is
TelemetryForwarderPort(internal/ports/outbound.go).
Why: the project focus shifted from storage to middleware/proxy behavior.
Both inbound and outbound adapters use transport-oriented names:
internal/adapters/inbound/httpinternal/adapters/outbound/httpinternal/adapters/inbound/natsinternal/adapters/outbound/nats
Why:
- In Go, package identity is the full import path, not only the package name.
- Direction is already explicit in folder path (
inboundvsoutbound). - In wiring code we alias imports for readability (
httpin,httpout,natsin,natsout).
Shared transport constants/helpers live under:
internal/adapters/shared/httpinternal/adapters/shared/nats
Examples:
- HTTP paths/headers/constants
- NATS subject constants and subject helpers
Why: avoids duplication while preventing inbound/outbound adapters from importing each other directly.
internal/application/telemetry/service.go uses:
newServiceWithDependencies(forwarder, deps)serviceDependenciesstruct for collaborators
Current collaborator:
normalizer
Defaults are applied inside constructor if collaborators are omitted.
Why:
- Tests can override exactly one collaborator without noisy setup.
- Constructor remains explicit and close to production wiring.
newServiceWithDependencies panics if forwarder is nil.
Why: wiring mistakes are programmer errors; fail immediately instead of panicking later deep in request handling.
Telemetry service has two test styles:
service_test.go(packagetelemetry): white-box/internal tests for collaborators and constructor behavior.service_blackbox_test.go(packagetelemetry_test): public API behavior from outside the package boundary.
Why:
service_test.gocan directly exercise internal constructors/collaborators.service_blackbox_test.goprotects public behavior and avoids over-coupling to internals.- Name
service_blackbox_test.gowas chosen intentionally (clearer thanintegrationfor this scope).
Entrypoint is in cmd/telemetry-playground/main.go (not cmd/main.go).
Why: idiomatic Go layout that scales to multiple binaries.
- Add new collaborator to
serviceDependencieswhen needed. - Keep defaults in constructor for low-friction tests.
- Add outbound adapters implementing
TelemetryForwarderPort. - Reuse
internal/adapters/shared/*only for true transport-shared concerns. - Keep domain free from adapter imports.
Run tests:
env -u GOROOT go test ./...Run playground:
go run ./cmd/telemetry-playground