About this article
This article is the entry point of the “Application Architecture” category in the Architecture Crash Course for the Generative-AI Era series, surveying the domain.
Before debating monolith vs microservices, if the code inside the app isn’t being written in a unified style, no overall structure works in practice. This article surveys what’s different from software architecture, the rules covering class design, naming, domain logic, and error handling, and the goal: “the whole team writes the same code.”
What is application architecture, anyway?
Picture a team kitchen. When five chefs share the same workspace, without rules like “knives go here,” “salt to this standard,” “plate in this order,” every dish comes out tasting and looking different.
Application architecture is the set of internal rules that lets the whole team write code the same way. Class design, naming conventions, where domain logic lives, error-handling policies — it aligns the “quality bar” of the code.
Without application architecture, each team member writes differently, and “is this style OK?” resurfaces at every review, dragging productivity down continuously.
Rules for “how to write the code”
Sometimes treated as part of software architecture, but folding it in there throws off the granularity balance, so it’s better to separate it. Decisions here are heavily influenced by software architecture choices, so consistency between the two matters.
Projects with weak application architecture get scattered code styles across the team, and “is this style OK?” debates flare up at every review. Boring conventions, enforced thoroughly, are what quietly sustain daily productivity and long-term maintainability.
Software vs Application
flowchart TB
subgraph SW["Software Architecture (outer skeleton)"]
SW1[Monolith vs Microservices]
SW2[Language selection]
SW3[API design]
SW4[DB selection]
end
subgraph APP["Application Architecture (inner rules)"]
APP1[Class design / SOLID]
APP2[Domain logic]
APP3[Naming]
APP4[Error handling]
end
SW -->|provides assumptions| APP
SW -.|One-way Door<br/>hard to change|.- L1[Decided at requirements]
APP -.|Two-way Door<br/>easier to change|.- L2[Decided at implementation]
classDef sw fill:#dbeafe,stroke:#2563eb;
classDef app fill:#fef3c7,stroke:#d97706;
class SW,SW1,SW2,SW3,SW4 sw;
class APP,APP1,APP2,APP3,APP4 app;
The names sound similar, but software architecture and application architecture are designs at fundamentally different granularities. Software architecture decides the app’s overall skeleton — monolith or microservices, the language, API design, database selection — the outer framework. Application architecture decides, within that framework, how the code itself gets written: class splits, naming conventions, how domain logic is expressed.
The reason for separating them is that the responsible decision-maker and the cost of change are very different. Software architecture has a lot of One-way Door decisions architects deliberate on during requirements. Application architecture is settled at implementation time, led by tech leads / lead engineers, and is mostly Two-way Door — relatively easy to redo.
| Aspect | Software Architecture | Application Architecture |
|---|---|---|
| Granularity | App’s external structure | App’s internal structure |
| Examples | Monolith / microservices / language / API design | Class design / naming / domain logic |
| When decided | Project planning to requirements | Design through implementation |
| Ease of change | Very hard (One-way Door) | Relatively easy (Two-way Door) |
In housing terms: software architecture is the floor plan (how many rooms, where the entrance is, whether plumbing is centralized); application architecture is “furniture layout, outlet positions, wallpaper patterns.” Changing the floor plan means knocking down walls; furniture moves easily. And day-to-day livability is decided by furniture, not the floor plan.
Why separate them
Mixing the two means the abstraction level of every discussion is uneven and meetings stop converging. You’re discussing “should we adopt microservices?” when “how should the classes inside split?” cuts in, or you’re settling naming conventions when “actually, the language choice…” drags everyone backward. “Aligning the granularity of decisions” is a precondition for moving design discussions forward.
This is exactly where early-career engineers get stuck — they assume “if I get class design right, the architecture gets better.” In reality, when the outer decisions about microservices or DB separation are wrong, no amount of clean class structure recovers it. Conversely, an architect who decides only the outside and leaves the inside open ends up with each team writing differently — eventually unmaintainable. The starting point is to run both layers in parallel as separate concerns.
- The software side gets dragged into fine implementation rules and the big-picture design wobbles.
- The application side stays vague and each team’s implementation drifts during development.
A healthy split: “architect” vs “lead engineer / tech lead”, each owning a different layer’s decisions.
What you must decide — what’s your project’s answer?
For each of the following, articulate your project’s answer in 1-2 sentences. Leaving them ambiguous now will always come back as “why did we decide that?” later.
Internal structure / domain design
| Item | Examples |
|---|---|
| Class composition | SRP / inheritance / composition / mixins |
| Domain logic | Procedural (Transaction Script) / Rich Domain (DDD — Domain-Driven Design) |
| Error-handling policy | Exceptions / Result types / Error boundary design |
| Naming and conceptual model | Ubiquitous language / glossary maintenance |
| Code conventions | Formatter / linter / review policy |
Code organization / test / observability
| Item | Examples |
|---|---|
| Directory layout | Feature-based / Layer-based |
| Dependency direction | No cycles / Clean Architecture |
| Test strategy | Unit / integration / E2E balance |
| Validation design | Which layer / how to reuse |
| Logging / audit | Log granularity / correlation IDs / PII (Personally Identifiable Information) masking |
Selection by case
The right amount of effort to put into application architecture varies a lot by project. Going for strict design across the board is over-engineering; leaving it alone collapses maintainability. Allocate investment by situation.
| Case | Where to focus |
|---|---|
| New CRUD-centric app | Naming + linter/formatter enforcement. Procedural domain logic is fine. |
| Complex business logic (insurance, finance, accounting) | Heavy domain model (DDD). Prioritize aligning code with business vocabulary (ubiquitous language). |
| Multi-team microservices | Unify directory layout, API contracts, error conventions across teams. |
| Legacy refactoring | Respect existing naming and structure; apply new conventions only to newly added pieces. |
Bringing DDD into a new CRUD app is the canonical over-engineering case — simpler procedural code is more maintainable. In a complex domain, the procedural style explodes in conditional branches and becomes unmaintainable, so domain-model investment pays off. For legacy refactors, “respect for existing conventions” comes first; applying new rules in one big sweep is a failure pattern.
The two main domain-logic styles
| Style | Trait | Fits |
|---|---|---|
| Procedural (Transaction Script) | Just function-shaped flow. Simpler than DDD. | Small / CRUD-centric / short-term |
| Rich Domain (DDD) | Business logic concentrated inside domain objects | Large / complex business logic / long-term ops |
“Start small, migrate to rich domain when it gets complex” is also a normal path. Avoiding over-engineering means not starting with DDD.
The Clean Architecture idea
The principle is to “point dependencies inward” so that changes in the outer layers (DB, UI, framework) don’t ripple into the business logic.
flowchart TB
subgraph OUT["Outer (changes often)"]
FW["Framework / DB / UI"]
subgraph ADAPT["Interface adapter layer"]
subgraph USE["Use cases"]
ENT["Entities<br/>(innermost = stable)"]
end
end
end
FW -->|dependencies only point inward| ADAPT
ADAPT --> USE
USE --> ENT
classDef out fill:#fee2e2,stroke:#dc2626;
classDef adapt fill:#fef3c7,stroke:#d97706;
classDef use fill:#dbeafe,stroke:#2563eb;
classDef ent fill:#dcfce7,stroke:#16a34a,stroke-width:2px;
class OUT,FW out;
class ADAPT adapt;
class USE use;
class ENT ent;
Scale × business-complexity ladder
The investment needed in application architecture changes with codebase size and business complexity. Conclusion up front: up to 10K LOC, plain layered is enough. Cross 50K LOC and Hexagonal pays off. Only at the 200K-LOC level for finance/insurance does serious DDD pay back. As of 2026 this path has the best ROI.
| Codebase | Complexity | Recommended pattern | Domain expression | File-line target |
|---|---|---|---|---|
| Up to 10K LOC | CRUD-centric | Layered / plain MVC | Transaction Script | <=300 lines |
| Up to 50K LOC | Moderate | Layered or Hexagonal | TS + Value Object | <=300 lines |
| Up to 200K LOC | Complex (e-commerce, logistics) | Hexagonal / Clean | Domain Model (light DDD) | <=300 lines |
| 200K LOC+ | Highly complex (finance, insurance) | Clean + tactical DDD | Full DDD (Aggregates, etc.) | <=300 lines |
”<=300 lines/file, <=50 lines/method, cyclomatic complexity 10” is the quantitative guardrail many projects use. Crossing those lines is the signal to split. The modern approach detects them automatically with ESLint or SonarQube. “Apply Clean’s 4 layers to a new CRUD screen” is canonical over-engineering.
Patterns escalate by scale. DDD on an MVP is excessive; procedural on a giant project breaks down.
Architecture-level traps
The forbidden moves at the application-architecture level:
| Forbidden move | Why |
|---|---|
| Growing god classes / god modules | ”While we’re here…” accumulates into 3000-line files; splitting later is essentially a rewrite. |
| Anemic domain model | DDD shape only — all logic ends up in service layers. |
| Leaving cyclic dependencies | A->B->A is a guaranteed bug-breeding ground; detect with ESLint. |
Names like Util / Manager / Helper | Names that don’t state responsibility; if responsibility is clear, use a concrete name. |
| Inheritance trees 4+ deep | LSP violations, ripple effects, hard to understand. |
Swallowing errors (catch { }) | Silent failure breeding ground; production-blind for cause. |
| Naming conventions different per team | The User / Member / Account / Customer problem. |
| Retries without idempotency | Double-payment, double-registration classic; Idempotency-Key required. |
| Comments saying WHAT | Don’t write what code already says — write WHY. |
| Linter / Formatter not enforced in CI, only in README | Conventions that aren’t enforced may as well not exist. |
| Assuming application design = class diagrams | Class diagrams are one artifact among many. Boring rules — naming, error policy, test strategy — drive daily productivity more. |
| Adding error handling after the fact | Error design always has gaps when bolted on. Which layer catches what is a first-class decision made alongside class design. |
Application architecture is decided by enforcement of conventions. Mechanical enforcement via tools is the only solution.
AI decision axes
| AI-era favorable | AI-era unfavorable |
|---|---|
| Strict types, schemas, interface definitions | Dynamic typing, implicit conventions |
| Unified directory layout and naming | Tribal style, per-team |
| Pure functions / side-effect separation (Hexagonal etc.) | Procedural code rich in side effects |
| Single-responsibility small classes / functions | Giant god classes / long methods |
Selection priority:
- Be consistent with software architecture (respect outer constraints).
- Prioritize internal consistency (enforcement over doctrine).
- Start small, harden as needed (DDD-first is over-engineering).
- Express types, conventions, structure in code (the AI-era requirement).
Summary
This article served as the entry point of the “Application Architecture” category, surveying the big picture of how to decide the internal rules.
Enforcement of conventions over choice of doctrine; gradual growth over over-engineering; design as a constraint on AI, not just for humans. Those are the three cores of application architecture.
The next articles dive deeper, starting with class design (SOLID, inheritance vs composition).
Back to series TOC -> ‘Architecture Crash Course for the Generative-AI Era’: How to Read This Book
I hope you’ll read the next article as well.
📚 Series: Architecture Crash Course for the Generative-AI Era (25/89)