TL;DR: A system of systems
I no longer design an application as one large set of files organized by convention. I design it as a system of systems: autonomous, closed, contractual boxes that are locally understandable and verifiable.
Each box has:
- a precise responsibility;
- a narrow public contract;
- a private implementation;
- its own tests;
- a
SOUL.mdthat explains what must survive; - an
AGENT.mdthat defines the maintainer’s authority; - a
MEMORY.mdthat maps what actually exists; - verifiable dependency boundaries;
- hard gates that authorize or refuse
DONE.
The most important piece is MEMORY.md.
This file is not optional documentation. It is an exhaustive, local, maintained map of the system: TypeScript files, functions, types, interfaces, classes, capabilities, contracts, entry points, dependencies, and — in the case of NestJS — providers and wiring.
Every maintainer is instructed to update it systematically, and a deterministic audit verifies that it still matches the filesystem and the real architecture.
My thesis is simple:
The more a codebase makes its boundaries and its state explicit, the less an agent has to guess them. The less it has to guess, the less maintenance depends on a huge context and fragile assumptions.
I consider this approach superior for the agentic maintenance of non-trivial systems, not because it contains more rules, but because it turns part of the architectural understanding into local, verifiable artifacts.
The problem is no longer just “writing good code”
Classic software engineering conventions remain necessary: typing, tests, SOLID, modularity, CI, code review.
But the arrival of coding agents introduces an additional problem.
An agent that receives:
"Change how payment reminders handle customers who already promised to pay."does not automatically know:
- which module owns the responsibility;
- which abstractions already exist;
- what is public or private;
- what it is allowed to modify;
- the invariants that previous developers wanted to preserve;
- the reasons behind certain decisions;
- the consumers that depend on a contract;
- the files that must absolutely not be touched.
It has to reconstruct this knowledge.
In many repositories, the natural strategy becomes:
Task
↓
search repository
↓
open files
↓
follow imports
↓
read neighboring services
↓
inspect shared utilities
↓
infer architecture
↓
make a changeThe problem is subtle: even if the final change touches four files, the agent may have inspected fifty.
Each additional file brings:
new concepts
+ implementation details
+ possible dependencies
+ alternative solutions
+ accidental opportunities to refactor
+ additional assumptionsThe model’s context progressively becomes larger than the actual problem.
It is a form of maintenance entropy.
My unit of architecture is not the file: it is the Box
A Box is not an arbitrary folder.
It is a capability important enough to have:
- its own meaning;
- its own responsibility;
- a contract;
- a lifecycle;
- a private implementation;
- tests;
- an identifiable owner;
- the ability to evolve independently.
Examples:
billing
customer-profile
payment-reminder
fraud-evaluation
verification-receipts
context-handoff
task-router
quality-gateOn the other hand:
parse-date
hash-string
normalize-namegenerally do not justify their own Box.
They are internal details of a larger capability.
So the question is never:
“How many boxes can I create?”
The question is:
“Where are the stable boundaries of responsibility?”
The contract matters more than the implementation
The central principle is this:
The public contract is the system’s promise to the rest of the architecture.
The inside can change.
The contract does not change silently.
Consumer ──▶ Contract ──▶ Implementation v1
└──▶ Implementation v2I want to be able to:
- change the algorithm;
- change the storage;
- replace a library;
- rewrite an adapter;
- optimize the code;
- change the internal structure;
without requiring consumers to know about that change.
It is the difference between:
consumer → implementation detailand:
consumer → contract ← implementationIn TypeScript:
export interface IInvoiceRepository {
save(invoice: Invoice): Promise<void>;
findById(id: InvoiceId): Promise<Invoice | null>;
}The consumer depends on IInvoiceRepository.
It has no reason to know whether the implementation uses Prisma, Drizzle, PostgreSQL directly, or an in-memory store in tests.
The local structure of a Box
A generic Box looks like this:
systems/<system-id>/
├── SOUL.md
├── AGENT.md
├── MEMORY.md
├── WIRING_PROTOCOL.md # conditional
├── contracts/
├── schemas/
├── src/
└── tests/The first three documents have different responsibilities.
| Artifact | Question it answers |
|---|---|
SOUL.md | Why must this system stay this way? |
AGENT.md | What is the maintainer responsible for, and where may it write? |
MEMORY.md | What actually exists right now? |
They must not duplicate each other.
SOUL.md: what must survive
SOUL.md describes the system’s reason for being.
Example:
# System identity
System: payment-reminder
## Mission
Decide and execute authorized payment reminders for overdue invoices.
## Why this system is a closed box
Consumers must not know provider-specific delivery logic, retry internals,
or storage implementation.
## Public promise
Given a valid reminder request, the system produces an explicit delivery result.
## Survival invariants
- Never send a reminder without an authorized policy decision.
- Never expose vendor-specific clients in the public contract.
- Preserve idempotency for retried reminder jobs.
## Non-responsibilities
- Owning customer contact data.
- Calculating invoice balance.
- Deciding accounting policy.This file plays an important role for agents.
Without it, an agent can produce a solution that is locally correct but architecturally destructive.
For example, it might “simplify” an indirection that existed precisely to prevent future coupling.
SOUL.md preserves the reason behind the boundary.
MEMORY.md: the most important piece for maintenance
This is the part of the architecture I value most for agentic coding.
MEMORY.md answers:
What actually exists in this Box?
It is not a summary.
It is not a conversation log.
It is not “read it if you have time” documentation.
It is an exhaustive, verifiable local map.
At a minimum, it must contain:
- the short mission;
- what the Box provides;
- what it consumes;
- the public API;
- the contracts;
- the entry points;
- the TypeScript files;
- their role;
- the important functions;
- the types and interfaces;
- the invariants;
- the dependencies;
- the verification commands;
- the active architectural decisions.
Example:
# System memory
system_id: payment-reminder
## Provides
- `PAYMENT_REMINDER_API` → `IPaymentReminderApi`
## Consumes
- `CUSTOMER_CONTACT_READER` → `ICustomerContactReader`
- `INVOICE_READER` → `IInvoiceReader`
## Public API
| Symbol | File | Role |
|---|---|---|
| IPaymentReminderApi | contracts/payment-reminder-api.ts | Public consumer contract |
| PAYMENT_REMINDER_API | contracts/tokens.ts | Runtime DI token |
## TypeScript symbol map
| File | Symbols | Visibility | Role |
|---|---|---|---|
| contracts/payment-reminder-api.ts | IPaymentReminderApi | public | Consumer contract |
| domain/reminder-policy.ts | ReminderPolicy, decideReminder | private | Deterministic reminder decision |
| application/send-reminder.ts | SendReminderService | private | Orchestrates delivery |
| infrastructure/email.adapter.ts | EmailReminderAdapter | private | Email delivery |
| tests/reminder-policy.test.ts | — | test | Policy behavior verification |The goal is for a fresh maintainer to understand the real structure before digging deep into src/.
Why MEMORY must be exhaustive
An incomplete MEMORY can be more dangerous than no MEMORY at all.
Suppose:
filesystem
├── evaluate-policy.ts
├── send-reminder.ts
└── security-check.tsbut:
MEMORY
├── evaluate-policy.ts
└── send-reminder.tsAn agent that uses the MEMORY to reduce context may conclude that security-check.ts does not exist.
It may then rebuild logic that is already present, or bypass a critical check.
So:
MEMORY.md
=
human-readable
+
machine-verifiableThe goal is not to have beautiful documentation.
The goal is to have a reliable representation of the current system.
The mandatory MEMORY.md maintenance rule
Every Box contains an explicit instruction:
## Mandatory maintenance rule
This MEMORY.md is part of the system's maintained state.
Whenever you add, remove, rename, move or materially modify:
- a TypeScript file;
- a function;
- a type;
- an interface;
- a class;
- a capability;
- a public contract;
- a dependency;
- an entry point;
update the relevant MEMORY.md maps before the task can be considered complete.
Do not update MEMORY.md from intention.
Update it from the final verified implementation.For NestJS, this list is extended to:
modules
controllers
providers
DI tokens
ports
adapters
guards
interceptors
pipes
filters
gateways
provider scopes
module exports
cross-box bindingsBut I do not trust the instruction alone.
An LLM can forget.
The rule must be verified by a machine.
The Memory Completeness Gate
The filesystem is compared to the MEMORY.
Filesystem / AST ──┐
├──▶ Memory auditor ──match──────────────▶ PASS
MEMORY.md maps ────┘ └──unmapped or stale──▶ FAILExample result:
export type MemoryAuditResult =
| {
readonly status: 'PASS';
readonly systemId: string;
readonly mappedFiles: number;
}
| {
readonly status: 'FAIL';
readonly systemId: string;
readonly unmappedFiles: readonly string[];
readonly staleFiles: readonly string[];
};Example error:
[MEMORY_FILE_UNMAPPED] TypeScript file is missing from system memory
where : systems/payment-reminder/src/new-policy.ts
cause : file exists but no matching MEMORY.md entry was found
fix : add the file and its architectural role to MEMORY.mdThe task cannot be considered done as long as:
unmappedFiles.length > 0
OR
staleFiles.length > 0The maintenance workflow I want
Instead of:
TASK
↓
search the repo
↓
infer architecture
↓
modify code
↓
hope assumptions were correctI want:
User task
↓
Impact resolver
↓
Resolve owning Box
↓
Load SOUL.md
↓
Load AGENT.md
↓
Load MEMORY.md
↓
Load public contracts
↓
Load relevant local source and tests
↓
Implement change
↓
Update MEMORY.md
↓
Run hard gates
├── PASS → DONE authorized
└── FAIL/BLOCKED → Correct / migrate / handoffThe difference looks simple, but it profoundly changes the role of context.
The agent no longer needs to start by discovering the world.
The system gives it a bounded cognitive surface.
Example: going from 500 files to a local context
Imagine:
500 source files
25 bounded systemsThe request:
"Change how repeated payment promises affect reminder escalation."Without an explicit boundary, the agent may search for:
promise
reminder
invoice
customer
escalation
notification
email
paymentand quickly end up in:
src/
shared/
utils/
billing/
customer/
notifications/
cron/
workers/
tests/With the Boxed Architecture:
TASK
↓
owner = payment-reminder
↓
load:
SOUL.md
AGENT.md
MEMORY.md
contracts/
reminder-policy.ts
send-reminder.ts
relevant testsThe goal is not to guarantee “exactly eight files”.
The goal is for the size of the context to be determined by the Box’s responsibility, not by the size of the repository.
Why I consider this superior for maintenance
I do not claim that a universally superior architecture exists.
I consider this approach superior when the problem is:
- a codebase that is going to live;
- several agents or developers;
- frequent changes;
- a need for handoff;
- coupling risks;
- domains complex enough that global exploration becomes expensive.
The main benefit is not initial creation speed.
It is locality of change.
13.1 Less mental reconstruction
The agent does not have to rediscover:
ownership
public surface
internal files
entry points
dependencies
local invariants
verification commandson every task.
13.2 Smaller blast radius
A local request ideally produces a local diff.
13.3 More reliable handoff
The previous maintainer does not need to stay in the context.
A fresh agent can take over the Box from its artifacts.
13.4 Long-term maintenance
A change of AI model, provider or human developer must not destroy local knowledge.
13.5 Better substitutability
An adapter can be replaced as long as the contract holds.
13.6 Verifiable architecture
A boundary that only exists in a diagram is easy to violate.
A boundary tested by AST, dependency graph and write scope is harder to bypass by accident.
But discipline is only useful if it is verifiable
An agent can say:
"Tests pass. Everything is done."That is not sufficient proof.
My chain of authority is:
LLM claim
↓
Machine verification
↓
Evidence
↓
Gate receipt
↓
Aggregate decision
├── PASS → DONE may be authorized
└── FAIL/BLOCKED → State rejectedThe principle is:
Soft reasoning prevents bad decisions. Hard gates prevent bad state.
The LLM can reason, propose, implement and diagnose.
But when a property can be verified deterministically through:
- the filesystem;
- Git;
- the AST;
- TypeScript;
- a test runner;
- a schema;
- a dependency graph;
I prefer machine proof.
The gates that protect maintenance
A local pipeline can include:
| Gate | Question |
|---|---|
| Bootstrap | Did the maintainer load the right context? |
| Write scope | Did it write only inside its owner’s scope? |
| Memory completeness | Does the MEMORY represent the current system? |
| Dependency boundaries | Were foreign internals imported? |
| Local tests | Is local behavior still proven? |
| Contract compatibility | Did the public promise change? |
| Code style | Are the style invariants respected? |
| Value derivation | Was a dynamic value hardcoded? |
| Test integrity | Were tests weakened? |
| Integration | Is the capability actually wired in? |
The important point is that DONE is not a boolean declared by the agent.
It is a decision derived from the required evidence.
Errors must be designed for the human AND for the agent
An error like:
Something went wrongis almost useless.
I prefer:
[SYSTEM_BOUNDARY_VIOLATION] Private implementation imported across system boundary
where : systems/orders/application/checkout.ts
cause : imports systems/billing/infrastructure/prisma-invoice.repository.ts
fix : depend on the Billing public contract insteadA good error immediately answers:
WHAT happened?
WHERE?
WHY?
WHAT should I do next?It reduces “stack archaeology” for a human as much as for an LLM.
TypeScript: types should tell the story of the flow
I favor discriminated unions when several states are mutually exclusive.
Avoid:
export type SendReminderResult = {
success: boolean;
receipt?: ReminderReceipt;
error?: string;
};This structure allows inconsistent states.
Prefer:
export type SendReminderResult =
| {
readonly status: 'SENT';
readonly receipt: ReminderReceipt;
}
| {
readonly status: 'SKIPPED';
readonly reason: 'PROMISE_TO_PAY_ACTIVE' | 'BALANCE_SETTLED';
}
| {
readonly status: 'FAILED';
readonly reason: ReminderFailureReason;
readonly guidance: ActionableGuidance;
};The status is the discriminant.
The compiler then helps preserve exhaustiveness:
export function toReminderMetric(
result: SendReminderResult,
): ReminderMetric {
switch (result.status) {
case 'SENT':
return { outcome: 'delivery', value: 1 };
case 'SKIPPED':
return { outcome: 'policy_skip', value: 1 };
case 'FAILED':
return { outcome: 'failure', value: 1 };
}
}Derived values are not independent facts
Another frequent source of bugs in generated code is the plausible but frozen value.
Wrong example:
const overdueInvoiceCount = 3;if 3 actually depends on the current invoices.
The rule is:
Derived values are views of authoritative state, not independent facts.
Prefer:
export function countOverdueInvoices(
invoices: readonly Invoice[],
now: Date,
): number {
return invoices.filter((invoice) => isOverdue(invoice, now)).length;
}The same goes for:
- a score;
- a number of jobs;
- a computed status;
- a list of unblocked dependencies;
- a hash;
- a timestamp;
- a path derived from configuration.
The test must prove the input → output relationship, not merely certify a frozen value.
Full example: adding a second reminder channel
Let’s take a B2B SaaS case.
The payment-reminder Box currently sends emails.
The product request is:
"Add WhatsApp for some customers without breaking existing email reminders."19.1 A quick but fragile implementation
An agent might naturally write:
@Injectable()
export class ReminderService {
constructor(
private readonly prisma: PrismaService,
private readonly whatsapp: WhatsAppClient,
private readonly email: EmailClient,
) {}
async remind(invoiceId: string): Promise<void> {
const invoice = await this.prisma.invoice.findUnique({
where: { id: invoiceId },
include: { customer: true },
});
if (invoice?.customer.whatsappEnabled) {
await this.whatsapp.send(invoice.customer.phone, 'Pay your invoice');
return;
}
await this.email.send(invoice!.customer.email, 'Pay your invoice');
}
}Why I don’t want this solution:
- the use case knows about storage;
- the customer domain leaks into the reminder Box;
- vendors are coupled to the use case;
- the text is hardcoded;
- results are implicit;
- no port allows the delivery to be substituted;
- tests have to mock too much infrastructure;
- a future change to a third channel will widen the service even further.
19.2 Contracts
export type ReminderChannel = 'EMAIL' | 'WHATSAPP';
export type ReminderMessage = {
readonly customerId: CustomerId;
readonly channel: ReminderChannel;
readonly destination: string;
readonly content: string;
readonly idempotencyKey: string;
};
export type ReminderDeliveryResult =
| {
readonly status: 'DELIVERED';
readonly providerMessageId: string;
}
| {
readonly status: 'FAILED';
readonly reason: 'TEMPORARY_PROVIDER_FAILURE' | 'INVALID_DESTINATION';
};
export interface IReminderDelivery {
deliver(message: ReminderMessage): Promise<ReminderDeliveryResult>;
}19.3 Pure policy
export type ReminderCandidate = {
readonly customerId: CustomerId;
readonly email: string | null;
readonly whatsappNumber: string | null;
readonly whatsappEnabled: boolean;
};
export function selectReminderChannel(
candidate: ReminderCandidate,
): ReminderChannel | null {
if (candidate.whatsappEnabled && candidate.whatsappNumber !== null) {
return 'WHATSAPP';
}
if (candidate.email !== null) {
return 'EMAIL';
}
return null;
}19.4 Private adapters
@Injectable()
export class WhatsAppReminderAdapter implements IReminderDelivery {
constructor(
@Inject(WHATSAPP_CLIENT)
private readonly client: IWhatsAppClient,
) {}
async deliver(
message: ReminderMessage,
): Promise<ReminderDeliveryResult> {
const response = await this.client.send({
to: message.destination,
body: message.content,
idempotencyKey: message.idempotencyKey,
});
return response.ok
? {
status: 'DELIVERED',
providerMessageId: response.messageId,
}
: {
status: 'FAILED',
reason: response.reason,
};
}
}The email adapter can implement the same contract.
The choice of adapter can be made by an internal router or by separate ports, depending on the real needs of the domain.
I do not introduce a pattern just to “do architecture”: it must correspond to a variation that actually exists.
19.5 Updating the MEMORY
The maintainer adds:
| File | Symbols | Visibility | Role |
|---|---|---|---|
| infrastructure/whatsapp-reminder.adapter.ts | WhatsAppReminderAdapter | private | WhatsApp delivery adapter |
| domain/select-reminder-channel.ts | selectReminderChannel | private | Deterministic channel policy |Then the Memory Gate verifies that the map is complete.
The outcome matters: the architecture and its map evolve together.
NestJS: adapt the philosophy to the framework, don’t fight the framework
My generic TypeScript style favors factories and functional composition.
But NestJS explicitly relies on decorated classes for several fundamental primitives.
Forbidding all classes in a NestJS codebase would be dogmatic and counterproductive.
So I separate:
Architecture invariants
│
├── framework-independent
│ contracts
│ ownership
│ MEMORY
│ boundaries
│ gates
│
└── implementation idioms
├── pure TypeScript → functions/factories preferred
└── NestJS → framework classes allowedNestJS documents that its modules encapsulate their providers, and that exported providers form the surface accessible to consumer modules.
I use this property as a first layer, then add stricter architectural rules on top.
A NestJS Box
systems/
└── billing/
├── SOUL.md
├── AGENT.md
├── MEMORY.md
├── contracts/
│ ├── billing-api.contract.ts
│ ├── billing.tokens.ts
│ └── invoice-repository.port.ts
├── application/
│ └── create-invoice.service.ts
├── domain/
│ ├── invoice.ts
│ └── money.ts
├── infrastructure/
│ └── prisma-invoice.repository.ts
├── transport/
│ └── billing.controller.ts
├── billing.module.ts
└── tests/The Nest module becomes the local composition root.
It wires.
It does not own the business logic.
NestJS: interfaces + tokens
A TypeScript interface disappears at runtime.
For a cross-box dependency, I therefore use a type contract plus a runtime token.
export const BILLING_API = Symbol('billing.api');
export interface IBillingApi {
createInvoice(input: CreateInvoiceInput): Promise<CreateInvoiceResult>;
}Implementation:
@Injectable()
export class CreateInvoiceService implements IBillingApi {
// ...
}Wiring:
@Module({
providers: [
CreateInvoiceService,
{
provide: BILLING_API,
useExisting: CreateInvoiceService,
},
],
exports: [BILLING_API],
})
export class BillingModule {}Consumer:
@Injectable()
export class CheckoutService {
constructor(
@Inject(BILLING_API)
private readonly billing: IBillingApi,
) {}
}What the consumer never does:
import { CreateInvoiceService }
from '../../billing/application/create-invoice.service';and even less:
import { PrismaInvoiceRepository }
from '../../billing/infrastructure/prisma-invoice.repository';Nest’s ability to inject a class does not make that class architecturally public.
NestJS: MEMORY.md also maps the wiring
A NestJS Box has more runtime metadata than a pure TypeScript package.
The MEMORY must therefore include:
## Nest module
| Module | File | Role |
|---|---|---|
| BillingModule | billing.module.ts | Local composition root |
## Controllers
| Controller | Route | Calls |
|---|---|---|
| BillingController | POST /invoices | BILLING_API |
## Providers
| Provider | Visibility | Role | Scope |
|---|---|---|---|
| CreateInvoiceService | private | Invoice issue use case | DEFAULT |
| PrismaInvoiceRepository | private | Persistence adapter | DEFAULT |
## DI tokens
| Token | Contract | Bound implementation | Visibility |
|---|---|---|---|
| BILLING_API | IBillingApi | CreateInvoiceService | public |
| INVOICE_REPOSITORY | IInvoiceRepository | PrismaInvoiceRepository | private |This lets an agent quickly see:
what is public
what is private
what is injected
what implementation backs a port
what Nest exports actually exposeNestJS: classes are allowed, coupling is not
The exact rule is:
Use classes where NestJS mechanics benefit from classes. Do not use NestJS as a reason to expose concrete implementations across Box boundaries.
Legitimate classes:
@Module()
@Controller()
@Injectable()
guards
interceptors
pipes
filters
gateways
strategies managed by the frameworkA pure function remains preferable for a pure rule:
export function calculateLateFee(
invoice: Invoice,
policy: LateFeePolicy,
): Money {
// deterministic domain rule
}I don’t turn this into a LateFeeCalculationService just because Nest knows how to inject classes.
Why I distrust forwardRef()
NestJS provides forwardRef() to resolve certain cycles.
I do not consider it a normal architectural strategy.
If I get:
BillingModule → OrdersModule
OrdersModule → BillingModuleI first look for:
- a misplaced responsibility;
- a missing contract;
- an orchestration that belongs to a third capability;
- an event/artifact that should decouple the workflow.
A forwardRef() may be necessary in some cases, but it then becomes a documented exception, not a habit.
A boundary only really exists if it resists tooling
Saying:
"don't import Billing internals"is a convention.
Making CI fail if:
systems/orders/**
→ imports
systems/billing/infrastructure/**is a constraint.
I prefer to generate the rules from a registry when possible:
{
"version": 1,
"systems": [
{
"id": "billing",
"root": "systems/billing",
"publicRoots": [
"systems/billing/contracts"
],
"ownerRole": "billing-maintainer"
}
]
}The registry does not replace MEMORY.md.
It serves the harness to:
resolve owner
build write scope
build dependency policy
route task
identify public rootsThe MEMORY remains the rich local map intended for the maintainer.
Local maintenance vs migration
This is a critical distinction.
If I change the implementation while preserving the contract:
maintenanceIf I change the public promise:
migrationExample:
export interface IBillingApi {
createInvoice(input: CreateInvoiceInput): Promise<CreateInvoiceResult>;
}If I replace Prisma with direct PostgreSQL: maintenance.
If I change:
createInvoice(...)to force every consumer to provide a new mandatory public field: potentially a migration.
I want the agent to stop and handle that consequence explicitly instead of hiding it inside a “small refactor”.
Cross-system: don’t turn the orchestrator into a super-agent
Some features genuinely touch several systems.
Example:
Orders
↓
Billing
↓
NotificationThat does not mean I give a giant agent the right to modify the whole repository.
The work can be split:
orchestrator
│
├── Billing migration / change
│ ↓
│ Billing maintainer
│
├── Orders consumer update
│ ↓
│ Orders maintainer
│
└── integration verificationEach owner keeps its boundary.
The cross-system change becomes an orchestration of contracts and receipts.
What this architecture costs
It is not free.
It requires:
- more discipline;
- explicit contracts;
- local artifacts;
- gates;
- a registry;
- maintenance of
MEMORY.md; - dependency boundaries;
- a clear distinction between maintenance and migration.
For a 2,000-line application that will be thrown away in a week, I probably would not build all of this.
The cost becomes worthwhile when:
lifetime ↑
team/agents ↑
change frequency ↑
domain complexity ↑
risk ↑
repository size ↑So I treat this architecture as an investment in the future cost of change.
The risks of my own approach
A serious architecture must also describe its failure modes.
31.1 Too many boxes
If every function becomes a box, you replace a monolith with a bureaucracy of micro-systems.
Remedy: a box must represent an autonomous capability, not a helper.
31.2 Stale MEMORY.md
This is the most serious risk.
Remedy: deterministic audit + a freshness/completeness gate.
31.3 Overly abstract contracts
An interface created “just in case” adds complexity without benefit.
Remedy: introduce an abstraction when a real boundary or axis of variation exists.
31.4 Too many documents
SOUL.md, AGENT.md and MEMORY.md can end up repeating the same thing.
Remedy: distinct documentary responsibilities.
31.5 False sense of security
A well-documented box can still contain a bug.
Remedy: tests, evidence, hard gates, integration verification.
31.6 Framework leakage
In NestJS, it is easy to turn modules into a facade of coupled classes.
Remedy: treat exports as a real API and verify cross-box imports.
What I measure to know whether it works
I don’t want to evaluate this architecture on its visual elegance.
I want to measure maintenance.
A few useful metrics:
files loaded per local task
cross-box files modified per local task
unauthorized write attempts
boundary violations
MEMORY drift incidents
contract migrations per change
median local change diff size
handoff success with a fresh agent
percentage of tasks completed without repo-wide explorationThe most interesting test is simple:
Can a fresh maintainer correctly modify an important Box without having to understand the entire repository?
If the answer is no, the cognitive boundary is not yet strong enough.
My main quality criterion
A good architecture is not just one in which the current code works.
I am looking for an architecture in which:
change is local
contracts are visible
ownership is explicit
context is bounded
knowledge survives handoff
errors are actionable
proof is machine-verifiableIn other words:
The cost of understanding a local change should scale with the local system, not with the entire repository.
This is the core of how I think about maintenance with AI agents.
Practical checklist: creating a new Box
Before creating a Box:
- does it have a real, autonomous responsibility?
- is there a useful contract boundary?
- can it evolve internally without exposing its details?
- does it have local tests?
- can an owner be identified?
Then create:
SOUL.md
AGENT.md
MEMORY.md
contracts/
src or application/domain/infrastructure
tests/Then define:
- public roots;
- write scope;
- authorized consumers;
- consumed dependencies;
- verification commands;
- memory audit;
- dependency boundary gate.
Practical checklist: maintaining a Box
Before coding:
- resolve the owner;
- read
SOUL.md; - read
AGENT.md; - read
MEMORY.md; - read the public contracts;
- load only the relevant local files.
During the change:
- stay within the write scope;
- preserve the contract if possible;
- do not import foreign internals;
- do not introduce a speculative abstraction;
- derive values from their source of truth;
- produce actionable errors.
Before DONE:
- update
MEMORY.md; - verify that it matches the filesystem;
- run the local tests;
- verify the boundaries;
- verify contract compatibility;
- verify the real integration;
- obtain the required receipts.
Example of a complete, compact NestJS MEMORY.md
# System memory
system_id: billing
## Mission
Own invoice issuance and invoice-level payment state.
## Provides
- BILLING_API → IBillingApi
## Consumes
- CUSTOMER_READER → ICustomerReader
- CLOCK → IClock
## Public API
| Token | Contract | File |
|---|---|---|
| BILLING_API | IBillingApi | contracts/billing-api.contract.ts |
## Nest module
| Module | File | Role |
|---|---|---|
| BillingModule | billing.module.ts | Local composition root |
## Controllers
| Controller | File | Route | Calls |
|---|---|---|---|
| BillingController | transport/billing.controller.ts | POST /invoices | BILLING_API |
## Providers
| Provider | File | Visibility | Role | Scope |
|---|---|---|---|---|
| CreateInvoiceService | application/create-invoice.service.ts | private | Invoice issuance | DEFAULT |
| PrismaInvoiceRepository | infrastructure/prisma-invoice.repository.ts | private | Persistence adapter | DEFAULT |
## DI tokens
| Token | Contract | Binding | Visibility |
|---|---|---|---|
| BILLING_API | IBillingApi | CreateInvoiceService | public |
| INVOICE_REPOSITORY | IInvoiceRepository | PrismaInvoiceRepository | private |
## TypeScript symbol map
| File | Symbols | Visibility | Role |
|---|---|---|---|
| contracts/billing-api.contract.ts | IBillingApi | public | Consumer contract |
| contracts/billing.tokens.ts | BILLING_API, INVOICE_REPOSITORY | mixed | Runtime tokens |
| application/create-invoice.service.ts | CreateInvoiceService | private | Use case orchestration |
| domain/invoice.ts | Invoice, createInvoice | private | Domain model |
| infrastructure/prisma-invoice.repository.ts | PrismaInvoiceRepository | private | Persistence |
| transport/billing.controller.ts | BillingController | private | HTTP adapter |
| billing.module.ts | BillingModule | entry | Nest wiring |
## Entry points
- Nest module: BillingModule
- Public token: BILLING_API
## Invariants
- No foreign system accesses invoice storage directly.
- No consumer imports infrastructure/**.
- HTTP DTOs never become domain contracts.
## Verification
- pnpm test -- billing
- G01 write scope
- G02 memory completeness
- G03 dependency boundaries
- G05 contract compatibility
- G13 integration
## Mandatory maintenance rule
Whenever a file, function, type, interface, class, provider, controller,
DI token, adapter, module export, entry point or cross-box dependency changes,
update this MEMORY.md from the final verified implementation before DONE.The final mental model
I summarize my architecture like this:
Repository
│
├── Box A
│ ├── public contract
│ ├── private implementation
│ ├── SOUL
│ ├── AGENT
│ ├── MEMORY
│ ├── tests
│ └── gates
│
├── Box B
│ ├── public contract
│ ├── private implementation
│ └── ...
│
└── Composition / orchestrationSystems do not collaborate by sharing their internals.
They collaborate through contracts.
Agents do not automatically receive the entire repository.
They receive a context bounded by ownership.
Documentation is not merely narrative.
An important part of it is auditable.
And DONE is not the model’s opinion.
It is an authorization derived from evidence.
Conclusion
I believe the next generation of software architecture will have to optimize two things at the same time:
- human understanding;
- maintainability by agents with limited, non-persistent context.
That does not mean writing code “for the LLM” at the expense of developers.
On the contrary, the properties I am looking for are just as useful to humans:
- clear responsibilities;
- narrow contracts;
- less coupling;
- actionable errors;
- local changes;
- reliable documentation;
- explicit ownership;
- reproducible verification.
The difference is that the agent makes the cost of ambiguity much more visible.
An implicit architecture can be gradually understood by a stable team over months.
A fresh agent has to rebuild it every time.
My approach therefore consists of turning as much of that tacit knowledge as possible into boundaries, contracts, local memory and proof.
More operational constraints can mean less behavioral entropy.
And for me, that is where the main advantage lies:
maintenance that is more local, more predictable and more transferable — even when the maintainer changes with every task.
Pure TypeScript vs NestJS
| Topic | Pure TypeScript | NestJS |
|---|---|---|
| Composition | factories/functions | @Module() + DI + functions |
| Classes | exception | allowed for framework primitives |
| Public contract | interface/type | interface/type + runtime token |
| Wiring | explicit factory composition | provider registration |
| Private implementation | module internals | non-exported providers + internal folders |
| Cross-box dependency | contract import | exported token + contract |
| Pure domain logic | functions preferred | functions still preferred |
| Circular dependency | forbidden | forwardRef() is not normal architecture |
| MEMORY | files/functions/types/interfaces | + modules/controllers/providers/tokens/scopes/exports |
Anti-patterns I want to detect
[SCOPE_FEATURE_CREEP]
[SCOPE_OPPORTUNISTIC_REFACTOR]
[SCOPE_HIDDEN_CONTRACT_CHANGE]
[TYPE_ANY_ESCAPE]
[TYPE_DUPLICATED_KNOWLEDGE]
[DERIVED_VALUE_HARDCODED]
[DUPLICATED_SOURCE_OF_TRUTH]
[FOREIGN_INTERNAL_IMPORT]
[FOREIGN_STORAGE_ACCESS]
[CROSS_SYSTEM_WRITE_BYPASS]
[LEAKY_PUBLIC_CONTRACT]
[GOD_INTERFACE]
[HIDDEN_GLOBAL_STATE]
[CIRCULAR_DEPENDENCY_INTRODUCED]
[PREMATURE_ABSTRACTION]
[SILENT_FAILURE_MASKED]
[FAIL_OPEN_VERIFICATION]
[TEST_EXPECTATION_WEAKENED]
[TEST_DISABLED]
[CAPABILITY_NOT_WIRED]
[UNVALIDATED_EXTERNAL_INPUT]
[NEST_GOD_SERVICE]
[NEST_EXPORT_EVERYTHING]
[NEST_CROSS_BOX_CONCRETE_INJECTION]
[NEST_FORWARD_REF_AS_ARCHITECTURE]
[NEST_HTTP_DOMAIN_COUPLING]Pack references
The doctrine in this tutorial distills my own internal documentation set, split across these documents:
BOXED_SYSTEM_ARCHITECTURE.mdBOXED_SYSTEM_OWNERSHIP_RULES.mdBOXED_SYSTEM_OWNERSHIP_IMPLEMENTATION_PLAN.mdCODE_STYLE_RULES.mdFORBIDDEN_IMPLEMENTATION_PATTERNS.mdHARD_GATES.mdVALUE_DERIVATION_RULES.md
The translation of these principles to NestJS is formalized in:
NESTJS_BOXED_ARCHITECTURE_RULES.md
Verified framework references: