Skip to content

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.


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

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.

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:

  1. Backend DI Container (apps/backend/.../di/container.ts): Instantiates database connections (SQLite, DuckDB pool) and injects them into domain services like FifoMaterializerService. Hono route handlers resolve dependencies from this container.
  2. Frontend Vue Injection: Adapters are provided at the root of the Vue application using strongly-typed InjectionKey symbols (e.g. VAULT_PORT_KEY). Composables consume them via inject() without coupling the view to the network transport.

  1. 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.
  2. Zero any Allowed: All layers are compiled under strict: true with zero type escapes.
  3. No Global BFF Client in UI: Vue components never import bffClient directly; they interact only with domain-bound composables.