Designing Software Architecture
Software Architecture Design
When asked to architect a system, produce these six artifacts in order:
- Architecture Layer Diagram (presentation → application → domain → infrastructure)
- Module Relation Map (which modules depend on which, and why)
- Event Flow (producers, event names, consumers, delivery guarantees)
- API Flow (request → gateway → service → response, including auth)
- Deployment Flow (CI/CD → environments → runtime topology)
- Folder Structure (concrete, per-module, matching the layers above)
Never output just prose. Always ground the design in these six sections, using diagrams-as-text (ASCII or mermaid) plus explanation.
Progress:
- Clarify domain scope and bounded contexts
- Define architecture layers (presentation/app/domain/infra)
- Identify modules and draw dependency relations (must be acyclic)
- Decide sync vs async boundaries → design event flow for async parts
- Design API contracts (REST/gRPC/GraphQL) and request lifecycle
- Design deployment topology (containers, orchestration, scaling policy)
- Generate folder structure matching modules and layers
- Validate: no circular deps, no god-modules, single responsibility per service/module
Step Details
1. Bounded Contexts First
Before drawing anything, list the core domains (e.g., Order, Payment, Inventory, User). Each becomes a module or service boundary. Never let one module own two unrelated domains.
2. Architecture Layers Use a strict layering per module:
Presentation → controllers, REST/GraphQL handlers, DTOs
Application → use cases, orchestrators, command/query handlers
Domain → entities, value objects, domain services, domain events
Infrastructure→ repositories, external clients, message brokers, ORM
Dependency rule: outer layers depend on inner layers, never reverse (Dependency Inversion via interfaces/ports).
3. Module Relations
Draw as a directed graph. Flag and eliminate cycles. Shared logic goes into a shared-kernel or common module — never cross-import between domain modules directly.
4. Event Flow For anything crossing service/module boundaries asynchronously:
[Order Service] --OrderCreated--> [Event Bus/Kafka] --> [Payment Service]
--> [Inventory Service]
Specify: event name, payload schema, delivery guarantee (at-least-once/exactly-once), idempotency strategy on consumers.
5. API Flow
Client --> API Gateway --> Auth Middleware --> Rate Limiter --> Service Controller
--> Application Layer (use case) --> Domain Layer --> Infra (DB/external)
<-- Response DTO <-- Application <-- Domain
6. Deployment Flow
Git Push --> CI (lint/test/build) --> Container Registry --> CD
--> Staging (smoke test) --> Production (rolling/blue-green)
Runtime: API Gateway --> Load Balancer --> Service Pods (auto-scaled) --> Managed DB/Cache/Broker
7. Folder Structure Mirror layers and modules exactly. Example for a modular monolith (Node/TS style, adapt per stack):
src/
modules/
order/
presentation/
order.controller.ts
application/
create-order.usecase.ts
order.query-handler.ts
domain/
order.entity.ts
order.events.ts
order.repository.interface.ts
infrastructure/
order.repository.ts
order.mapper.ts
payment/
... (same shape)
shared/
events/
event-bus.ts
kernel/
base-entity.ts
config/
bootstrap/
Example 1: Input: "Design architecture for an e-commerce checkout system with order, payment, and inventory."
Output:
- Layers: standard 4-layer per module (presentation/application/domain/infrastructure)
- Module Relation:
Orderdepends onPayment(via port/interface) andInventory(via port);PaymentandInventorydo not depend onOrder(inverted via events to avoid coupling) - Event Flow:
OrderCreated→ Kafka →Payment.Reserve,Inventory.Reserve;PaymentConfirmed/InventoryReserved→Order.Confirm; saga/orchestrator pattern for rollback on failure - API Flow:
POST /checkout→ Gateway → Auth →CheckoutController→CreateOrderUseCase→ emits domain event → returns202 Acceptedwith order tracking id - Deployment: 3 independently deployable services behind API Gateway, each auto-scaled, Kafka as shared broker, Postgres per service (database-per-service pattern)
- Folder Structure: as shown in Quick Start, one top-level module per bounded context
Example 2: Input: "Refactor a monolith user-management module that's grown too large."
Output:
- Diagnosis: identify hidden bounded contexts inside (e.g.,
Authentication,Profile,Permissions) — split into 3 modules - Module Relation:
Authenticationis upstream (no deps);ProfileandPermissionsdepend onAuthenticationvia interface, not concrete class - Event Flow:
UserRegisteredemitted byAuthentication, consumed byProfile(create profile) andPermissions(assign default role) — decouples sync creation logic - API Flow: unchanged externally (facade
UserControllercomposes the three use cases) to avoid breaking API contracts - Deployment: stays in-process for now (modular monolith), but module boundaries are drawn so each can be extracted to a microservice later without domain rewrite
- Folder Structure: split
modules/user/intomodules/authentication/,modules/profile/,modules/permissions/
- Always design ports/interfaces in domain layer; concrete implementations live in infrastructure — enables testability and swapping providers.
- Prefer async events for cross-domain side effects; use sync API calls only within the same bounded context or for strict read-after-write needs.
- Every service boundary implies: its own datastore (or schema), independent deployability, and an explicit contract (API/event schema) — no shared database across services.
- Design for idempotency on every event consumer (dedupe key, upsert semantics).
- Include observability in deployment flow by default: structured logging, tracing (correlation id propagated through API and event flows), metrics/alerts.
- Version APIs and event schemas from day one (
/v1/..., schema registry) — production-grade means backward compatibility is planned, not retrofitted. - Keep the folder structure output literal and copy-pasteable — architects implement faster from concrete trees than abstract descriptions.
- Don't design a "shared domain module" that multiple bounded contexts import directly — this recreates monolith coupling with extra steps.
- Don't skip the event flow section just because a system looks simple — even simple systems benefit from explicit async boundaries for scalability.
- Don't propose deployment topology without addressing scaling policy, health checks, and rollback strategy — "deploy to Kubernetes" alone is not production-grade.
- Don't let API layer talk directly to infrastructure (e.g., controller calling ORM directly) — always route through application/domain layers.
- Don't output folder structures that don't match the module/layer design stated earlier in the same response — they must be consistent.
- Don't default to synchronous chained API calls between services for workflows that span multiple domains — this creates fragile temporal coupling; use events/sagas instead.