@startuml Hexagonal Architecture Example
title Hexagonal Architecture Example
package App {
class ApplicationContext {
+ userRepository: UserRepository
}
class Application {
- applicationContext: ApplicationContext
+ users: Users
}
class Users <<serviece>> {
- userRepository: UserRepository
+ addUser()
}
interface UserRepository <<port>> {
+ save(user: User): User
}
class User <<struct>> {
+ id: string
+ name: string
}
}
package Main {
class MongoUserRepository <<adapter>> {
- dbConnection: DbConnection
+ save(user: User): User
}
class ProdApplication {
- userRepository: UserRepository
}
}
note left of ProdApplication {
An entrypoint index.ts or main.ts
file would bootstrap the application
by creating the ApplicationContext with
the appropriate adapters and passing it to
the Application.
}
package Test {
class TestUserRepository <<adapter>> {
- users: User[]
+ save(user: User): User
}
class TestApplication {
- userRepository: UserRepository
}
}
note right of TestApplication {
An entrypoint test file would
create the ApplicationContext with the
TestUserRepository and pass it to the
Application for testing purposes.
}
Application --> ApplicationContext : requires
Application --> Users : contains
Users --> UserRepository : depends on
UserRepository <|.. TestUserRepository : implements
UserRepository <|.. MongoUserRepository : implements
Users --> User : manages a collection of
TestApplication ..> TestUserRepository : instantiates
ProdApplication ..> MongoUserRepository : instantiates
TestApplication <|.. Application : extends
ProdApplication <|.. Application : extends
@enduml
Last active
January 8, 2026 11:32
-
-
Save JonathanTurnock/53504402aa342b66d472f40963de2d6c to your computer and use it in GitHub Desktop.
Hexagonal Architecture for LLMs (Typescript).md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # llms.txt — Agentic Workflow Architecture & Testing Guidelines | |
| # Purpose: Provide clear, machine-readable conventions for LLM-driven agentic workflows | |
| # that implement domain logic using Hexagonal Architecture (Ports & Adapters), | |
| # exposed via a single entrypoint, with strong testing practices. | |
| ## North Star | |
| - Keep **domain logic pure** and independent of frameworks, IO, databases, network, and UI. | |
| - Express all external interactions through **ports** (interfaces). | |
| - Implement ports in **adapters** (infrastructure). | |
| - Wire everything together in **exactly one entrypoint** (composition root). | |
| - Test the domain through **port substitutes** (in-memory adapters / fakes), not real infra. | |
| ## Architectural Constraints (Hard Rules) | |
| 1. **Single Entrypoint (Composition Root)** | |
| - There MUST be one production bootstrap file (e.g. `src/main.ts` or `src/index.ts`) that: | |
| - Creates adapter instances | |
| - Builds the `ApplicationContext` | |
| - Instantiates `Application` | |
| - Exposes the app’s public API (HTTP handlers, CLI, jobs, etc.) | |
| - No other module may instantiate infrastructure adapters directly. | |
| 2. **Domain Depends Only on Ports** | |
| - Domain services (use-cases) MUST depend only on port interfaces. | |
| - Domain MUST NOT import: | |
| - DB clients, ORMs, HTTP clients, SDKs, filesystem APIs, environment variables, loggers, timers | |
| - test frameworks | |
| - web frameworks (Express/Fastify/Next/etc.) | |
| 3. **Ports Are Stable Contracts** | |
| - Ports must be small and purpose-driven. | |
| - Ports define domain-shaped inputs/outputs (prefer domain types), not DB shapes. | |
| - Ports must be synchronous or async as required, but consistent per port. | |
| 4. **Adapters Are Replaceable** | |
| - Adapters implement ports and map from domain types to infrastructure types. | |
| - Adapters MUST NOT contain domain policy/decision logic. | |
| 5. **No Hidden Global State** | |
| - No module-level singletons for repositories, DB connections, or config. | |
| - Configuration flows from the entrypoint into adapters/context. | |
| 6. **Context Is a Dependency Container, Not a Service Locator** | |
| - `ApplicationContext` is constructed once at bootstrap and passed downward. | |
| - Domain services receive only what they need (ports), not an entire context, deps may optionally be expressed as a type rather than long constructor params. | |
| ## Recommended Project Layout | |
| - `src/app/` | |
| - `entities/` (e.g. `User`) | |
| - `ports/` (e.g. `UserRepository`) | |
| - `services/` (use-cases, e.g. `Users`) | |
| - `errors/` (domain errors) | |
| - `ApplicationContext` (holds ports) | |
| - `Application` (exposes orchestrated API/use-cases) | |
| - `src/adapters/...` (infrastructure implementations) | |
| - `src/adappters/**/*.test.ts` (Mockist Tests) | |
| - `src/main.ts` (ONLY place where adapters are wired) | |
| - `src/__tests__/` | |
| - `adapters/` (in-memory fakes) | |
| - `*.test.ts` (Sociable Tests for the Application API) | |
| ## Canonical Pattern (Hexagonal) | |
| - App: | |
| - Entities: `User` | |
| - Port: `UserRepository` | |
| - Service: `Users` (use-case) | |
| - `ApplicationContext` holds `userRepository: UserRepository` | |
| - `Application` owns `users: Users` | |
| - Adapters: | |
| - `MongoUserRepository implements UserRepository` | |
| - `TestUserRepository implements UserRepository` | |
| - Entrypoints: | |
| - Prod entrypoint: creates `MongoUserRepository` and bootstraps `Application` | |
| - Test entrypoint: creates `TestUserRepository` and bootstraps `Application` | |
| ## Entrypoint Requirements | |
| Production entrypoint MUST: | |
| - Read configuration (env/files) ONLY here | |
| - Create infra (db connection, clients) | |
| - Construct adapters | |
| - Construct `ApplicationContext` | |
| - Construct `Application` | |
| - Export the public entry API (e.g. `createServer(app)` or `handler(app)`) | |
| Test entrypoint MUST: | |
| - Build `ApplicationContext` with in-memory adapters | |
| - Instantiate `Application` | |
| - The Tests MUST work with both the TestApplication but also the ProdApplication (With test setup) | |
| - Tests implementations cannot use additional methods (i.e. Clearing) so must focus on fresh insantiation per test | |
| ## Testing Policy (Architecture + Practice) | |
| ### Test Pyramid (Required) | |
| 1. **Sociable Testing (Most)** | |
| - Test `App` for maximum coverage using social testing. | |
| - Create Two Test Cases: | |
| - Instantiate `TestApplication` via a test entrypoint. | |
| - Instantiate `ProdApplication` via a test entrypoint. | |
| - Assert behavior across multiple services/use-cases using Test Iteration i.e. describe.each or test.each | |
| 2. **Adapter Contract Tests (Some)** | |
| - For each adapter, verify it satisfies the port contract: | |
| - persistence semantics | |
| - error mapping | |
| - idempotency expectations | |
| - Use real dependencies only when necessary (e.g. ephemeral containers). | |
| ### Testing Rules (Hard) | |
| - Do not mock domain services; mock/replace ports instead. | |
| - Every domain service MUST be testable by constructing it with port fakes. | |
| - Avoid snapshot testing for domain logic. Prefer explicit assertions. | |
| - Tests MUST NOT depend on wall-clock time; inject time via a port if needed (e.g. `Clock`). | |
| - Property-based tests are encouraged for invariant-heavy entities. | |
| ### Required Test Coverage Targets (Guidance) | |
| - Domain services: cover happy path + key edge cases + failure mapping. | |
| - Ports: define clear expected behavior in tests (see “Port Contracts” below). | |
| - Entrypoint wiring: at least one integration test to ensure correct adapter wiring. | |
| ## Port Contract Documentation (Must-Have) | |
| For each port interface, document: | |
| - Method purpose | |
| - Input validation expectations (domain-level) | |
| - Return semantics (e.g. does `save` return stored entity? does it assign ids?) | |
| - Error semantics (domain errors vs adapter errors) | |
| - Consistency and transactional assumptions | |
| - Performance constraints if relevant | |
| Example (UserRepository.save): | |
| - Accepts a domain `User` | |
| - Persists user (create or update per domain rules) | |
| - Returns persisted `User` (including any assigned id) | |
| - Throws/returns `UserAlreadyExists` only if required by domain rules | |
| ## Error Handling & Mapping | |
| - For known errors, prefer the use of Neverthrow https://www.npmjs.com/package/neverthrow | |
| - If not available use a typed error and return it i.e. readFile(path: string): string | ReadFileError | |
| - Throwing errors is OK but only for `unchecked` exceptions | |
| - App defines domain errors (e.g. `UserAlreadyExists`, `ValidationError`). | |
| - Adapters translate infra errors into domain errors (or explicit adapter errors that are mapped at the boundary). | |
| - Entrypoint maps domain errors to transport errors (HTTP codes, CLI exit codes). | |
| ## Agentic Workflow Guidance (LLM-Specific) | |
| When generating or modifying code: | |
| 1. **Start in the app** | |
| - Define/adjust entities, ports, and services first. | |
| 2. **Keep IO at the edges** | |
| - If a feature requires IO, add/extend a port. | |
| Agent should prefer: | |
| - use of ports | |
| - explicit dependency injection | |
| - avoiding global singletons | |
| - avoid reliance on mocking | |
| ## Anti-Patterns (Disallowed) | |
| - Importing DB/HTTP clients in domain or app services | |
| - Creating adapters inside services/use-cases | |
| - Reading `process.env` outside entrypoint | |
| - Reading from a config file and then importing it | |
| - “God context” passed everywhere | |
| - Mocking internals of domain services instead of using ports and adapters | |
| - Logic in adapters that decides domain outcomes | |
| ## Output Expectations | |
| When asked to implement features: | |
| - Provide: | |
| - domain changes + tests | |
| - adapter implementations (if needed) | |
| - entrypoint wiring changes | |
| - Avoid: | |
| - framework-specific coupling in domain | |
| - touching multiple composition roots (there should be one) | |
| ## See the following PlantUML example | |
| @startuml Hexagonal Architecture Example | |
| title Hexagonal Architecture Example | |
| package App { | |
| class ApplicationContext { | |
| + userRepository: UserRepository | |
| } | |
| class Application { | |
| - applicationContext: ApplicationContext | |
| + users: Users | |
| } | |
| class Users <<serviece>> { | |
| - userRepository: UserRepository | |
| + addUser() | |
| } | |
| interface UserRepository <<port>> { | |
| + save(user: User): User | |
| } | |
| class User <<struct>> { | |
| + id: string | |
| + name: string | |
| } | |
| } | |
| package Main { | |
| class MongoUserRepository <<adapter>> { | |
| - dbConnection: DbConnection | |
| + save(user: User): User | |
| } | |
| class ProdApplication { | |
| - userRepository: UserRepository | |
| } | |
| } | |
| note left of ProdApplication { | |
| An entrypoint index.ts or main.ts | |
| file would bootstrap the application | |
| by creating the ApplicationContext with | |
| the appropriate adapters and passing it to | |
| the Application. | |
| } | |
| package Test { | |
| class TestUserRepository <<adapter>> { | |
| - users: User[] | |
| + save(user: User): User | |
| } | |
| class TestApplication { | |
| - userRepository: UserRepository | |
| } | |
| } | |
| note right of TestApplication { | |
| An entrypoint test file would | |
| create the ApplicationContext with the | |
| TestUserRepository and pass it to the | |
| Application for testing purposes. | |
| } | |
| Application --> ApplicationContext : requires | |
| Application --> Users : contains | |
| Users --> UserRepository : depends on | |
| UserRepository <|.. TestUserRepository : implements | |
| UserRepository <|.. MongoUserRepository : implements | |
| Users --> User : manages a collection of | |
| TestApplication ..> TestUserRepository : instantiates | |
| ProdApplication ..> MongoUserRepository : instantiates | |
| TestApplication <|.. Application : extends | |
| ProdApplication <|.. Application : extends | |
| @enduml | |
| # End of llms.txt |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment