Built by the creator of tx|Primitives for memory, tasks & orchestrationVisit tx docs
tx-agent-kit
Architecture

DDD Construction Pattern

How domain-driven design is implemented in tx-agent-kit

tx-agent-kit implements domain-driven design (DDD) with a specific construction pattern that separates concerns into well-defined layers. This pattern is enforced mechanically, not just documented.

Directory structure

Each domain in packages/core/src/domains/ follows this structure:

packages/core/src/domains/<domain>/
  domain/         # Entities, value objects, pure domain rules
  ports/          # Interfaces and capability contracts
  application/    # Use-case orchestration
  adapters/       # External system adapters (optional)
  runtime/        # Layer wiring (optional)
  ui/             # Presentation-facing helpers (optional)

Persistence implementations live separately in packages/db/src/repositories/, not inside the domain slice.

Layer responsibilities

domain/

The innermost layer containing pure domain logic with zero dependencies on infrastructure:

// packages/core/src/domains/billing/domain/invoice.ts

export interface InvoiceRecord {
  readonly id: string
  readonly workspaceId: string
  readonly amount: number
  readonly currency: string
  readonly status: InvoiceStatus
  readonly createdAt: Date
}

export type InvoiceStatus = "draft" | "sent" | "paid" | "cancelled"

export const canTransition = (
  from: InvoiceStatus,
  to: InvoiceStatus,
): boolean => {
  const transitions: Record<InvoiceStatus, ReadonlyArray<InvoiceStatus>> = {
    draft: ["sent", "cancelled"],
    sent: ["paid", "cancelled"],
    paid: [],
    cancelled: [],
  }
  return transitions[from].includes(to)
}

All functions in domain/ must be pure: no Date.now, new Date, Math.random, or any side effects. Only named exports are allowed (no default exports). The layer cannot import from packages/db, effect, or any adapter. All *Record interfaces that ports reference must originate here.

ports/

Abstract interfaces defining what capabilities the domain needs without specifying how they are implemented:

// packages/core/src/domains/billing/ports/invoice-repository.port.ts

import type { InvoiceRecord } from "../domain/invoice.js"

export type InvoiceRepositoryPort = {
  readonly findById: (id: string) => Effect.Effect<InvoiceRecord | null>
  readonly findByWorkspace: (workspaceId: string) => Effect.Effect<ReadonlyArray<InvoiceRecord>>
  readonly create: (input: CreateInvoiceInput) => Effect.Effect<InvoiceRecord>
  readonly updateStatus: (id: string, status: InvoiceStatus) => Effect.Effect<InvoiceRecord>
}

Port files must use export type and import record types from domain/. export interface declarations are banned. Ports can only import from domain/ and other ports/ files; they cannot reference concrete implementations.

application/

Use-case orchestration that composes domain logic with port-defined capabilities:

// packages/core/src/domains/billing/application/invoice.service.ts

import { canTransition } from "../domain/invoice.js"
import type { InvoiceRepositoryPort } from "../ports/invoice-repository.port.js"

export const makeInvoiceService = (repo: InvoiceRepositoryPort) => ({
  transitionStatus: (id: string, newStatus: InvoiceStatus) =>
    Effect.gen(function* () {
      const invoice = yield* repo.findById(id)
      if (!invoice) return yield* Effect.fail(new InvoiceNotFound({ id }))
      if (!canTransition(invoice.status, newStatus)) {
        return yield* Effect.fail(
          new InvalidStatusTransition({ from: invoice.status, to: newStatus })
        )
      }
      return yield* repo.updateStatus(id, newStatus)
    }),
})

The application layer can import from domain/, ports/, and other application/ files, but never from concrete infrastructure. It orchestrates calls to port methods and domain functions without containing SQL or HTTP calls itself.

Persistence in packages/db/src/repositories/

Concrete repository implementations live in the database package, not inside the core domain:

// packages/db/src/repositories/invoice.repository.ts

import type { InvoiceRepositoryPort } from "@tx-agent-kit/core/domains/billing/ports/invoice-repository.port.js"

export const makeInvoiceRepository = (db: DrizzleClient): InvoiceRepositoryPort => ({
  findById: (id) => Effect.tryPromise(() =>
    db.select().from(invoices).where(eq(invoices.id, id)).then(rows => rows[0] ?? null)
  ),
  // ... other methods
})

This separation ensures that packages/core has zero dependency on drizzle-orm or any database library.

What is NOT allowed in core domains

The structural invariant checks enforce these prohibitions:

ProhibitionRationale
No repositories/ directory inside packages/core/src/domains/Persistence belongs in packages/db
No services/ directory inside packages/core/src/domains/Use application/ instead for consistency
No imports from drizzle-orm, pg, or database-specific packagesCore must remain infrastructure-free
No process.env readsUse typed config modules
No console.* callsUse @tx-agent-kit/logging

On this page