Initializing
Back to Retrospectives

Architecture-First Development with AI: Why I Design Before I Prompt

AIArchitectureTypeScriptDevelopment ProcessGeminiModular Monolith

There's a failure mode that's becoming increasingly common as AI coding tools improve.

It goes like this: a developer opens Copilot or a chat interface, describes the feature they want, and accepts the generated code. The code works. The tests pass. The PR merges. Six months later, the codebase is a maze of poorly bounded modules, unclear ownership, and circular dependencies — all generated at high speed by a tool that had no context about the architectural intent.

I call this AI-accelerated technical debt. The debt accumulates faster than before because the *volume* of code you can produce has increased, but the *quality of the underlying design decisions* hasn't changed.

The solution isn't to use AI less. It's to use it differently.


The Rule I Follow

> Design the architecture first. Document the decisions. Then use AI to implement the design.

This is the core principle I apply on every project. The AI handles implementation — the scaffolding, the boilerplate, the repetitive patterns, the type signatures. I handle the architecture — the domain boundaries, the dependency direction, the data ownership, the failure modes.

The result: AI produces code that fits the design rather than code that *is* the design.


What "Architecture First" Actually Means in Practice

Before I write a single prompt or touch a code editor, I produce three things:

1. A Domain Map

I draw the bounded contexts — which domains exist, what each one owns, what it exposes to others, and what it never exposes.

For the Portfolio Nexus API, this looked like:

code
domains/
├── suggestions/Owns: Suggestion entity, moderation flow, rate limiting policy
├── github-stats/Owns: GitHub heatmap data, caching, proxy endpoint
├── admin/Owns: Auth, JWT cookie, admin actions (calls suggestions service)
└── spider-web/Owns: Config read, radar chart data endpoint

The rule: a domain may call another domain's *service interface*. It may never import from another domain's *internal files* (repository, policy, DTO).

I write this down as a diagram before any code exists. This takes 20–40 minutes. It saves 20–40 hours of refactoring later.

2. Architectural Decision Records (ADRs)

For each non-obvious structural choice, I write a short ADR:

markdown
# ADR-001: Modular Monolith over Microservices

## Context
The API serves 4 endpoints across 3 domains. The team is 1 developer.

## Decision
Build as a modular monolith. Domain isolation enforced by ESLint boundaries
plugin, not by network boundaries.

## Rationale
A microservices split at this scale adds deployment complexity, network latency,
and distributed transaction problems with no meaningful benefit.
The monolith can be split later if load actually demands it.

## Consequences
All domains share a process. Scaling is vertical until further notice.

This takes 10 minutes per decision. It's invaluable when I'm prompting AI three months later and need to explain "don't add a separate microservice for this."

3. A Dependency Direction Rule

Before generating any code, I define the allowed import graph:

code
RouterControllerServicePolicyRepositoryDatabase

                              (dependencies flow inward)
                              (domainnothing external)

Any generated code that violates this direction gets rejected immediately, regardless of whether it "works."


How I Actually Use AI to Implement

Once the design is documented, I use AI heavily — but with explicit context injection.

The Context Prompt Pattern

Every session starts with a context block:

code
You are implementing a specific domain module for a modular monolith Express.js API.

Architecture rules (non-negotiable):
- Domains import from other domains only through their index.ts public API
- Controllers are the ONLY files that import Express Request/Response/NextFunction
- Services contain business logic onlyno raw DB queries, no HTTP objects
- Repositories contain data queries onlyno business logic
- Policies are pure functionsno I/O, no async, no database calls
- All environment variables are read only from src/config/env.ts
- No 'any' types. No @ts-ignore.

Current domain: suggestions
Existing files: suggestions.service.ts, suggestions.policy.ts, suggestions.repository.ts
Task: implement the controller layer

Without this context, AI generates a controller that directly queries the database and puts business logic inline. With it, the generated code fits the architecture.

I Review Every Line

This is the non-negotiable part. I don't merge AI-generated code I haven't read.

This isn't skepticism about AI quality. It's professional responsibility. The code goes into a system I'm accountable for. I need to understand it so I can explain it, debug it, and modify it six months from now.

My review checklist for AI-generated code:

  • [ ] Does it follow the dependency direction rule?
  • [ ] Does it import only from permitted locations?
  • [ ] Is there any any type hiding in here?
  • [ ] Are error cases handled, or does it silently swallow errors?
  • [ ] Does it handle the edge cases I documented in the ADR?
  • [ ] Would I be comfortable explaining every line of this to a client?

If something fails this review, I correct it. Sometimes I prompt again with a correction. Sometimes I just rewrite the relevant section.

The "Fix This" Pattern

When AI generates code that's close but wrong in a specific way, I use targeted correction prompts rather than regenerating from scratch:

code
This code directly queries the database from the controller:

[paste the bad code]

Move the database query into SuggestionsRepository.findById().
The controller should only call this.service.getById(id) and return the result.
Keep the error handling structure the same.

This is faster than a full regeneration and produces more predictable output.


Where AI Genuinely Excels (And Where It Doesn't)

AI is exceptional at:

  • TypeScript type signatures for well-defined data shapes
  • Boilerplate that follows an established pattern (a 5th repository method when 4 exist)
  • Zod schema definition when the fields are specified
  • Test scaffolding (generate the test structure; I fill in the assertions)
  • Writing migration SQL from a schema diff description
  • Refactoring repetitive code to a consistent pattern once I've established what the pattern is

AI consistently struggles with:

  • Domain boundary decisions — it doesn't know which domain should own a concept
  • Cross-cutting concerns — it tends to handle them at the wrong layer
  • Failure mode analysis — it optimizes for the happy path
  • Long-term maintainability — it produces working code, not necessarily modifiable code
  • Context from 3 months ago — you have to re-inject the architecture every session

The implication: use AI for *within*-layer implementation, not for *across*-layer design decisions.


A Concrete Example: The GitHub Stats Module

When I needed to build the GitHub contribution heatmap proxy, I designed it first:

Decision (5 minutes):

  • Lives in domains/github-stats/
  • Public surface: one endpoint GET /api/github/contributions
  • Returns cached data with a 1-hour TTL (in-memory Map, v1)
  • Never exposes the GitHub token to the website
  • Falls back to empty array if the GitHub API is unreachable

ADR (10 minutes):

markdown
# ADR-005: In-Memory Cache for GitHub Heatmap

Context: GitHub API has rate limits. Heatmap data doesn't change by the minute.

Decision: Cache in-memory Map with 3600s TTL.

Rationale: Redis adds deployment complexity. The data is non-critical —
an empty heatmap on cache miss is acceptable. Post-v1, replace with Redis.

AI prompt (after design was locked):

code
Implement a GitHub stats service with these exact specifications:

Service: GitHubStatsService
- Method: getContributions()Promise<ContributionDay[]>
- Uses in-memory Map cache with 3600 second TTL
- Falls back to empty array [] if GitHub GraphQL API returns an error
- GITHUB_TOKEN comes from env.GITHUB_TOKEN (already validated in env.ts)
- No console.loguse logger.info/warn/error from shared/core/logger
- Follows the service layer rules: no HTTP objects, no raw DB queries

Type: ContributionDay = { date: string; count: number; level: 0|1|2|3|4 }

The generated service was ~85% correct. I corrected the error handling (it was swallowing errors silently) and added the proper logger calls. Total implementation time: 25 minutes including review and correction.

Without the design phase, I would have spent that 25 minutes debugging something that was structurally wrong.


The Bottom Line

AI is the best implementation accelerant I've encountered. It's also the most dangerous design accelerant I've encountered.

The discipline of architecture-first development — domain maps, ADRs, explicit dependency rules — doesn't become less important when AI is in the loop. It becomes *more* important, because the speed at which structurally wrong code can be generated has increased dramatically.

Design before you prompt. Document the decisions. Let AI fill the scaffold. Review every line.

That's the workflow. It's slower than pure AI generation and faster than pure manual development — and it produces codebases that survive contact with reality.


*The architectural patterns described here are implemented in the Portfolio Nexus — the full ADR set is available in the docs/ directory of the repository.*

Applied in Projects

Explore the real-world production systems and custom codebases where these engineering patterns are fully implemented.

fullstack

AATMANOVA Digital Platform — Spiritual Services & Numerology SaaS

NestJSReactVite
frontend

NGO-3 Website Migration — WordPress to Next.js Headless Conversion

Next.js 14React 18TypeScript
frontend

NGO-4 Website Migration — WordPress to Next.js Headless Conversion

Next.js 14React 18TypeScript
fullstack

Aatmanova Universe — Premium Wellness Platform

TypeScriptNext.js 15NestJS
backend

DarkMatter — Discord System Management Bot

TypeScriptNode.jsRust
fullstack

EMS Universe — Enterprise Management System

TypeScriptNestJSNext.js
devops

GATOR — AI-Powered Orchestration System

TypeScriptNode.jsHono
fullstack

HERALD — Social Media Automation System

TypeScriptNode.jsNestJS
fullstack

Namani Construction — Premium Building & Interior Design Platform

Next.jsNestJSTypeScript
devops

Orbit — AI-Powered Project Controller

TypeScriptNode.jsReact
fullstack

PMS — Multi-Tenant Portfolio Management Platform

Next.jsNestJSTypeScript
devops

Aatmanova Cloud Manager — Enterprise Rclone GUI

Node.jsExpressTypeScript
devops

Server Box — Infrastructure Management Platform

TypeScriptNode.jsHono
fullstack

4ME - Personal OS (Productivity OS)

NestJSNext.jsPostgreSQL
fullstack

MBTCMS — Municipal Board Tax Collection & Management System

NestJSNext.jsPostgreSQL
fullstack

AI Generated Frontmatter (Obsidian Plugin)

TypeScriptObsidian PluginAI/LLM
fullstack

Office File Locator (OFL)

FastAPIReactPostgreSQL
fullstack

Portfolio Nexus — The Homopedia Nexus

TypeScriptNext.js 14React 18
fullstack

Vedic Numerology Calculator (VNC)

Next.jsFastifytRPC
fullstack

Water Industry System (WIS) - Enterprise ERP

Next.jsTypeScripttRPC