Skip to main content
Archived design reference — not the current architecture

These docs describe a proposed Simplified Hexagonal Architecture from an internal design exploration that was not adopted. The design principles (entity invariants, contracts, dependency inversion, load → authorize → validate → execute, in-memory-fake testing) remain useful, but the specifics below do not exist in this codebase: the package paths packages/core / packages/infra / packages/shared, the @packages/* import aliases, the main.ts wiring entry point, and the EnableArchitectureTransition feature flag. Do not follow the paths, aliases, or imports here as-is.

Core Concepts

← Back to README

Architecture has 3 concepts: Core (business logic), Contracts (interfaces), Outside (implementations + entry points). Core lives in packages/core/, knows nothing about infra or HTTP. Wire everything together in the entry point at startup.


The Simplified Model​

We keep things simple with just 3 concepts:

┌─────────────────────────────────────┐
│ OUTSIDE │
│ (HTTP, DB, Files, APIs, etc.) │
│ │
│ ┌───────────────┐ │
│ │ CONTRACTS │ │
│ │ (Interfaces) │ │
│ └───────┬───────┘ │
│ │ │
│ ┌───────▼───────┐ │
│ │ CORE │ │
│ │ (Your App) │ │
│ └───────────────┘ │
└─────────────────────────────────────┘

The 3 Concepts​

ConceptDescriptionLocation
CoreYour business logic (domain objects, actions, contracts)packages/core/src/
ContractsInterfaces that define what external things you needpackages/core/src/{feature}/ or packages/core/src/shared/
OutsideImplementations of contracts + entry pointspackages/infra/src/ + apps/client/pages/api/

Monorepo Structure​

Why apps/ and packages/?​

This is a common monorepo convention that enforces architectural boundaries:

FolderPurposeContains
packages/Shared libraries - reusable code with no deployment contextBusiness logic, domain objects, contracts
apps/Deployable applications - runnable apps that wire packages with infrastructureHTTP server, CLI tools, workers

The key insight:

packages/ = WHAT your business does (pure, portable)
apps/ = HOW it's deployed (wired, specific)

Why this matters:

  • packages/core/ has zero dependencies on deployment context - it doesn't know if it's running in an API server, CLI, or test
  • apps/client/ is a specific deployment that wires core logic with real infrastructure
  • Enforces the "core knows nothing" rule at the folder level
  • Makes it trivial to add new entry points (CLI, workers, admin dashboards) that reuse the same core

Wiring It Together (Next.js Pages API)​

The entry point creates implementations and wires everything together.

// apps/client/pages/api/orders/index.ts
import { baseApi } from '@server/middlewares/baseApi';
import mongoose from 'mongoose';

// Infrastructure
import { OrderRepositoryMongo } from '@packages/infra/orders/OrderRepositoryMongo';
import { SendGridMailer } from '@packages/infra/shared/email/SendGridMailer';

// Core
import { createOrder } from '@packages/core/orders';
import { createOrderSchema } from '../validators/orderValidators';

const handler = baseApi({ auth: true })
.post(async (req, res) => {
// Validate input
const parsed = createOrderSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({ error: parsed.error.flatten() });
}

// Create implementations (fulfill contracts)
const orderRepository = new OrderRepositoryMongo();
const mailer = new SendGridMailer(
process.env.SENDGRID_API_KEY!,
'orders@myapp.com'
);

// Bundle dependencies
const deps = { repository: orderRepository, mailer };

// Call action
const order = await createOrder(deps, req.ctx, parsed.data);
res.status(201).json({ id: order.id });
});

export default handler;

Next Steps​