Hexagonal Architecture (Ports & Adapters)
🏛️ Hexagonal Architecture (Ports & Adapters)
Section titled “🏛️ Hexagonal Architecture (Ports & Adapters)”Kryptofolio strictly adheres to Hexagonal Architecture (also known as Ports and Adapters) across both its frontend presentation application and backend analytical service.
The primary architectural guarantee is Domain Independence: business logic does not know whether it is running inside Vue, a terminal CLI, or an automated testing suite.
🏗️ The Three Concentric Layers
Section titled “🏗️ The Three Concentric Layers”flowchart TD
subgraph Infrastructure ["3. Infrastructure Layer (Adapters)"]
RestAdapter["RestTaxAdapter"]
SqliteVault["SqliteVaultAdapter"]
DuckDbEngine["DuckDbMetricsAdapter"]
end
subgraph Application ["2. Application Layer (Use Cases)"]
UploadUseCase["UploadTaxFileUseCase"]
UnlockUseCase["UnlockVaultUseCase"]
SummaryUseCase["GetPortfolioSummaryUseCase"]
end
subgraph Domain ["1. Domain Layer (Pure Business)"]
Models["Entities & Value Objects\n(PreciseAmount, LotId)"]
Ports["Domain Ports (Interfaces)\n(ITaxPort, IVaultCredentialsPort)"]
end
Infrastructure --> Application
Application --> Domain
1. Domain Layer (packages/core-domain/)
Section titled “1. Domain Layer (packages/core-domain/)”The innermost core. It possesses zero framework dependencies (no Vue, no Hono, no Axios, no SQL libraries).
- Entities & Value Objects: Pure TypeScript interfaces and Branded Types (
PreciseAmount,LotId,AssetId). - Ports: Outgoing interfaces defining contracts for persistence, market data, and cryptography (
ITaxPort,ILedgerPort,IVaultCredentialsPort). The domain dictates what data it requires, never how it is fetched.
2. Application Layer (src/core/application/)
Section titled “2. Application Layer (src/core/application/)”Orchestrates domain interactions to satisfy user workflows:
- Use Cases: Pure TypeScript classes (e.g.
UploadTaxFileUseCase,UnlockVaultUseCase). - All state-changing mutations must pass through an explicit Use Case.
- Contains zero presentation code or reactivity primitives (no
ref(),reactive(), or Pinia imports).
3. Infrastructure Layer (src/core/infrastructure/)
Section titled “3. Infrastructure Layer (src/core/infrastructure/)”The outer boundary interacting with the outside world:
- Adapters: Concrete implementations of Domain Ports (e.g.
BffTaxAdapter,AesGcmCryptographyAdapter,DuckDbMetricsAdapter). - Anti-Corruption Layer (ACL): Zod validation schemas that sanitize and transform external API inputs before instantiating domain entities.
- Composition Root: Centralized Dependency Injection wiring.
🔄 End-to-End Data Flow Sequence
Section titled “🔄 End-to-End Data Flow Sequence”Here is the exact lifecycle of a user action (such as uploading a tax transaction file):
sequenceDiagram
autonumber
participant UI as Vue 3 Component
participant Colada as Composable (useMutation)
participant UseCase as UploadTaxFileUseCase
participant Port as ITaxPort (Domain Port)
participant Adapter as BffTaxAdapter (Infrastructure)
participant RPC as Hono RPC Client (hc)
participant Backend as Hono Backend Server
UI->>Colada: trigger upload(file, sourceProfile)
Colada->>UseCase: execute({ file, sourceProfile })
Note over UseCase: Validates business constraints<br/>and prepares ingestion payload
UseCase->>Port: uploadFile(payload)
Port->>Adapter: Invokes concrete implementation
Adapter->>RPC: bffClient.api.tax.upload.$post()
RPC->>Backend: HTTP POST /api/tax/upload
Backend-->>RPC: 200 OK (Processed lots)
RPC-->>Adapter: Strongly typed JSON response
Adapter-->>Port: Maps DTO to Domain Entity
Port-->>UseCase: Returns Domain Result
UseCase-->>Colada: Resolves mutation promise
Colada-->>UI: Updates reactive state (isSuccess)
🔌 Composition Root & Dependency Injection
Section titled “🔌 Composition Root & Dependency Injection”To avoid hardcoded instantiation of concrete classes inside components:
- Backend DI Container (
apps/backend/.../di/container.ts): Instantiates database connections (SQLite, DuckDB pool) and injects them into domain services likeFifoMaterializerService. Hono route handlers resolve dependencies from this container. - Frontend Vue Injection: Adapters are provided at the root of the Vue application using strongly-typed
InjectionKeysymbols (e.g.VAULT_PORT_KEY). Composables consume them viainject()without coupling the view to the network transport.
🛡️ Key Architectural Rules
Section titled “🛡️ Key Architectural Rules”- CQRS Principles: Simple read operations (Queries) may let Pinia Colada composables call the injected Domain Port directly. Write operations (Mutations) must always pass through a dedicated
UseCase. - Zero
anyAllowed: All layers are compiled understrict: truewith zero type escapes. - No Global BFF Client in UI: Vue components never import
bffClientdirectly; they interact only with domain-bound composables.