Architecture — AionApi¶
This document describes the architectural principles, component responsibilities, and runtime flows in AionApi. It is intended as a technical reference for maintainers and for guiding future extensions.
Summary
- Hexagonal (Ports & Adapters) architecture: core business logic is independent of transport and infrastructure.
- Primary adapters (HTTP / GraphQL) are thin; usecases contain business rules and orchestrations.
- Observability-first: OpenTelemetry tracing, Prometheus metrics, and Grafana dashboards.
- Consistent error model, standard response envelope, and strict use of context propagation.
High-level layout¶
Top-level layout (simplified):
AionApi/
├─ cmd/ # application entrypoints
├─ internal/ # bounded contexts and platform (bootstrap, server, config)
│ ├─ <bounded-context>/ # auth, user, category, tag, habit, admin ...
│ │ ├─ core/ # business logic
| | │ ├─ domain/ # entities, value objects
│ │ │ ├─ ports/ # input/output interfaces
│ │ │ └─ usecase/ # application services
│ │ ├─ adapter/ # primary (http/graphql) + secondary (db/cache/token)
│ └─ platform/ # config, bootstrap, server, observability
├─ infrastructure/ # docker, migrations, observability resources
├─ docs/ # MkDocs site
├─ swagger/ # OpenAPI artifacts
└─ tests/ # test helpers and generated mocks
Bounded contexts include auth, user, category, tag, habit, and admin. Each context defines its own domain model, ports, and adapters.
Design principles¶
- Explicit contracts: the core defines input and output ports (interfaces) so implementations can vary without changing business code.
- Single responsibility: adapters are responsible only for translation and transport concerns; usecases handle business rules.
- Testability: core logic is pure Go and easy to unit-test with table-driven tests + mocks for ports.
- Observability: every handler, usecase and repository opens a span and adds structured attributes.
- Config-driven timeouts and limits: no hard-coded timeouts; read from configuration with safe defaults.
Components and responsibilities¶
- Core (Usecases)
- Located at
internal/<ctx>/core/usecase. -
Implement application flows, validations, and orchestrations. Return domain values or typed semantic errors.
-
Primary adapters (HTTP / GraphQL)
- Located at
internal/<ctx>/adapter/primary. -
Decode requests, validate DTOs, start traces/spans, call usecases, map domain → transport response.
-
Secondary adapters (DB, Cache, Token, Logger)
- Located at
internal/<ctx>/adapter/secondary. -
Implement output ports declared by the core. They keep infra-specific code isolated (GORM, redis client, external SDKs).
-
Platform layer
-
internal/platformcontains bootstrap wiring, server setup (HTTP / GraphQL), shared middleware, config loader, and observability wiring. -
Shared packages
internal/shared/*holds shared helpers:sharederrors,httpresponse,constants, and test helpers.
Request lifecycle (detailed)¶
REST flow (example: Update password)¶
- HTTP request arrives at handler (
adapter/primary/http). - Handler parses the request and validates the DTO.
- Handler starts a trace span and sets common attributes (route, method, user_id if available).
- Handler calls the usecase input port with
context.Context. - Usecase executes domain logic, calling output ports (repositories, caches, token provider).
- Output adapters perform context-aware IO (e.g.
db.WithContext(ctx)) and map infra errors to semantic domain errors. - Usecase returns domain values or a semantic error.
- Handler maps results to a standardized JSON envelope (
internal/shared/httpresponse) and ends the span.
GraphQL flow (example: Create Category)¶
- Resolver → GraphQL handler → DTO mapping → usecase → repository → domain mapping → resolver returns GraphQL model.
- GraphQL middleware should also start a top-level span and propagate context to resolvers.
Error model and HTTP mapping¶
- The repository uses typed semantic errors (validation, not_found, conflict, unauthorized, internal, etc.) defined in
internal/shared/sharederrors. - The adapter maps these semantic errors to HTTP status codes consistently:
- validation → 400
- unauthorized → 401
- forbidden → 403
- not_found → 404
- conflict → 409
- internal → 500
- All responses use a consistent envelope shape to simplify clients and monitoring.
Persistence and migrations¶
- Migrations are SQL files under
infrastructure/db/migrationsand applied with themigrateCLI via Make targets (make migrate-up). - Repositories implement explicit mapping between DB models and domain models to keep the domain clean of ORM types.
- Repositories always use context-aware DB operations and return translated semantic errors.
- Soft deletes: the application ensures reads exclude logically deleted rows and updates return not-found when no rows were affected.
Authentication & security¶
- Authentication usecase issues access and refresh tokens through an
AuthProvideroutput port. - Tokens may be stored / referenced in a cache (
AuthStore) for revocation support. - HTTP authentication middleware validates
Authorization: Bearer <token>and populates context withuser_idand claims. - Secrets must never be logged (the logger and helpers ensure sensitive fields are redacted).
- Follow OWASP guidance for JWT usage, cookie flags and CORS policies.
Observability¶
- Tracing: OpenTelemetry spans are created at the handler and propagated into usecases and repositories. Naming convention:
aionapi.<context>.<component>(e.g.aionapi.user.usecase). - Metrics: Prometheus instrumentation and scrape config live under
infrastructure/observability/prometheus. - Dashboards & logs: Grafana dashboards and Fluent Bit/Loki configs are under
infrastructure/observability.
Suggested local env to enable OTLP export:
export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4318"
export OTEL_SERVICE_NAME="AionApi"
export OTEL_SERVICE_VERSION="0.1.0"
Testing strategy¶
- Unit tests: focus on usecases; use gomock-generated mocks for output ports (
tests/mocks), and helper suites intests/setup. - Integration tests: exercise adapters and real infra (run against Dockerized Postgres/Redis). Use a dedicated test DB and cleanup strategy.
- End-to-end / smoke tests: run against the full
make devstack and validate common flows (health, login, create user, basic GraphQL ops). - Coverage:
make test-coverproduces coverage artifacts intests/coverage/.
Code generation & developer tooling¶
- GraphQL: schema fragments live in contexts;
make graphqlcollects them and runsgqlgen. - Mocks:
make mocksrunsmockgenand stores generated mocks undertests/mocks. - Quality:
make lint,make lint-fix,make formatandgolangci-lintconfiguration are part of the developer workflow.
Adding a new bounded context — checklist¶
- Create
internal/<ctx>/core/domainfor entities and value objects. - Define input/output ports under
internal/<ctx>/core/ports. - Implement usecases in
internal/<ctx>/core/usecase. - Add primary adapters (
adapter/primary/httpand/orgraphql) for transport. - Add secondary adapters (
adapter/secondary/db,cache,token) implementing the output ports. - Wire concrete adapters in
internal/platform/bootstrap. - Mount routes in
internal/platform/server/http/composer.go. - Add tests and generate mocks (
make mocks). - Document the context in
docs/and updatemkdocs.ymlnav if needed.
Deployment and operational notes¶
- Use environment-specific config files for production vs staging vs local development.
- Keep secrets in a secure vault or secrets manager (do not commit to repo).
- CI jobs should run
make lint,make test, andmake docs.gen(if docs changes are present). - Consider blue/green or rolling deployment patterns for production services.
Diagram (Mermaid)¶
You can embed a Mermaid diagram into MkDocs (if plugin enabled). Example:
flowchart LR
Client -->|HTTP/GraphQL| Handler[Handler (primary adapter)]
Handler -->|calls| Usecase[Usecase (core)]
Usecase --> Repo[Repository (secondary adapter)]
Usecase --> AuthProvider[AuthProvider (secondary)]
Repo --> DB[(Postgres)]
AuthProvider --> Cache[(Redis)]
Conventions & style¶
- Always propagate
context.Contextand honor cancellations and deadlines. - Keep handlers thin: validation, tracing, DTO mapping, usecase invocation, response mapping.
- Keep usecases pure from infra concerns; use small interfaces for side effects.
- Avoid magic strings — centralize keys in
internal/shared/constants.