AK
WorkExperienceCase StudiesTimelineWritingContact
Resume
AK
All writing

Writing

Hexagonal DDD from Day One: Building Holocomm to Be Built by Agents

May 23, 2026·8 min read·Updated June 1, 2026

architecturedddmonorepomulti-tenancyai-agents

Holocomm is a multi-tenant, WhatsApp-first booking platform: a customer messages a business, an AI agent proposes times, answers questions, takes payment, and confirms the appointment. The product is a pnpm monorepo — a NestJS backend, a Next.js dashboard, an operator console, a shared contract package. But the monorepo holds more than the product. It also holds the AI agent that runs the conversations, the evaluation harness that grades it, and the autonomous loop that writes most of the code. Four systems, one repository, one set of rules.

That arrangement was a decision, made on day one: hexagonal DDD, everything in one place, no external dependency I did not control. This is why I made it, and the one thing that was genuinely hard.

Hexagonal DDD from day one

The architecture was not something Holocomm grew into. The domain / application / infrastructure / interface/http split, the ports, the Symbol() injection tokens, the one-way dependency rule — all of it was in place before the first feature shipped. For a single developer that reads as over-engineering, and for the first weeks it was.

Then I went full-agentic, and the calculus inverted. Once an autonomous loop and a fleet of subagents do the writing, I am no longer a solo developer — I am one person directing a team that happens to be made of agents. A team needs boundaries it cannot accidentally cross. Strict layering is what lets that team produce and maintain a system larger than one person could hold in their head: the boundaries are legible to the agents that write the code and enforceable by the agent that reviews it — the planner's Review Agent fails any plan that imports infrastructure into the domain or bypasses a port, before a line is written.

Strict boundaries pay off twice with machine-generated code. An agent handed a fresh context and a single layer to work in produces a narrow, checkable change. A reviewer holding the same rules rejects a violation before it lands. The architecture is the contract between me and the agents.

Why a monorepo

Three reasons, in the order they actually drove the decision.

Context. Agents are only as good as what they can see. In one repository, every agent — planner, implementer, reviewer — has the product, the shared contract, the conventions doc, and the eval suite within reach, with no boundary it has to be told to cross. Split the same code across repos and every task begins with the agent missing half the picture. The monorepo exists so the agents never work blind.

Ergonomics. For one person, a monorepo is the path of least resistance: one install, one pnpm check, one place to change a type in @holocomm/contract and watch it propagate through the backend, the dashboard, and the operator console at once.

No external dependency I don't control. The eval harness is in-repo, not a SaaS. The agent's memory is MongoDB, not a managed vector store. As far as possible the system depends on nothing I cannot read, seed, and run locally — which keeps it deterministic to evaluate and portable into whatever it becomes next.

Every outside system is a port

Inside that structure, the load-bearing rule is that nothing volatile reaches the core. Every external system — the Meta WhatsApp Cloud API, the payment provider, the AI model, Mongo itself — sits behind a port: an interface the domain defines and the outside world implements. Which implementation answers at runtime is a wiring decision, not a code change.

PortProduction adapterTest / dev adapter
WhatsAppProviderPortMetaCloudProvider (Graph API)MockWaProvider
WhatsAppWebhookParserPortMeta payload parserMockWaParser
*RepositoryPortMongo*Adaptermongodb-memory-server

The application layer never learns which one it got. In tests and local dev it talks to a mock that needs no credentials and no network; in production it talks to the real Graph API. The payoff is the day Meta changes its envelope, or a payment provider is swapped: the churn is contained to one file in infrastructure/, and the domain, the use cases, and every test against them stay untouched — because none of them ever knew the old backend's name.

Multi-tenancy you can't forget to apply

The worst bug in a multi-tenant product is the one where tenant A sees tenant B's data. Holocomm makes that structurally hard: the tenant id is never read from the request body, query, or URL — only from the X-Business-Id header — and one global guard proves membership on every authenticated route.

// common/guards/business-tenant.guard.ts (trimmed)
async canActivate(ctx: ExecutionContext): Promise<boolean> {
  if (this.isPublicOrSkipped(ctx)) return true;
 
  const req = ctx.switchToHttp().getRequest<Request>();
  const target = req.headers['x-business-id'] as string | undefined;
 
  // BusinessMembership is the sole tenant gate, read LIVE from Mongo on every
  // request — no cache — so revoking access takes effect immediately.
  const membership = await this.memberships.findByUserAndBusiness(
    req.user.id,
    target,
  );
  if (!membership || !membership.active) return false;
  await this.assertBusinessActive(target); // reject suspended / archived
 
  req.businessId = target; // controllers read it via the @Tenant() decorator
  return true;
}

Because the id arrives in exactly one place and is validated in exactly one place, an individual controller cannot forget to scope a query — it is handed an already-proven businessId. Getting here took iterations: an earlier params → body → query → header fallback chain was deleted on purpose, because every extra place a tenant id can come from is another place it can leak across tenants. Tenant isolation is the kind of thing that is only safe once there is exactly one way to do it.

Conversations as a concurrency-safe state machine

An AI agent negotiating a booking over WhatsApp is a distributed-systems problem wearing a chat interface. The agent proposes a slot; the customer takes ninety seconds to reply "yes"; meanwhile a 10-minute TTL might expire the offer, or staff might cancel it. Two writers, one truth. Each negotiation is modeled as a workflow with an explicit, monotonic state machine:

const ALLOWED_TRANSITIONS = new Map([
  [Proposed,             new Set([AwaitingConfirmation])],
  [AwaitingConfirmation, new Set([Confirmed, Cancelled, Expired, Failed])],
  [Confirmed,            new Set([Executed, Cancelled, Expired, Failed])],
]);
 
export function assertTransition(from, to) {
  if (TERMINAL_STATUSES.has(from) || !ALLOWED_TRANSITIONS.get(from)?.has(to)) {
    throw new InvalidTransitionError(from, to);
  }
}

Illegal jumps — a Confirmed workflow rewinding to Proposed — are impossible by construction. The races are handled with optimistic concurrency: the write lands only if the version read is still the version on disk.

const doc = await this.model.findOneAndUpdate(
  { workflowId, version: expectedVersion }, // ← the guard lives in the filter
  { $set: setFields, $inc: { version: 1 } },
  { new: true },
);
if (!doc) throw new ConcurrencyConflictError(workflowId, expectedVersion);

If the customer's "yes" and the TTL sweep both try to move the same workflow, one wins and the loser gets a ConcurrencyConflictError instead of silently clobbering state. No locks, no lost updates.

The hard part: a booking agent with no precedent

The architecture was the easy decision. Hexagonal DDD is a known shape; I could lean on it. The hard part — the part that is genuinely all me — is the booking agent.

There is no reference architecture for what it does. I could not find a product that books real appointments, takes real payments, and holds a multi-turn negotiation over WhatsApp the way I needed, and there are no public patterns for it — no post, no repo, no diagram to copy. Every decision had to be invented and defended without a precedent to fall back on: how the conversation is structured (a parent graph routing to booking, information, and account subgraphs); how the agent is stopped from confirming a booking it cannot actually make (a preflight trip-wire that blocks any mutating tool whose validator has not cleared it); how state survives ninety seconds of customer silence (a checkpointer per chat plus a short-lived cached state on the conversation).

That is exactly why the architecture mattered. When the thing you are building has no map, the one variable you can control is the ground you build it on. Hexagonal boundaries gave the novel part — the agent — a stable, well-understood frame to be invented inside. The evaluation harness exists for the same reason: with no external benchmark for "is this agent good," I had to build the benchmark too.

What the boundaries buy

This is a large amount of structure for one person: ports, tokens, adapters, a guard, a state machine, a contract package, an eval suite, a folder per concern. For a weekend project it would be absurd. Holocomm earns it because it is not really a one-person project — it is a one-person project executed by a team of agents, and that team needs its discipline made structural.

The payoff is the thing I could not have done alone: produce and maintain a system at team scale, with the parts most likely to hurt in production — multiple tenants, external integrations, money, and an AI making real commitments — turned into the parts the architecture will not let me, or the agents, get wrong. It is the same conviction reached from other directions: in the Vimbus loop, where verification is a gate the agents cannot skip, and in evaluation-driven development, where the eval suite is the spec. Make the correct outcome the only one the system accepts, and a single person plus a team of agents can build something that did not exist before.