AgentSkillsCN

01 Architecture And System Design

01 架构与系统设计

SKILL.md

01 — Architecture & System Design

Description

Decompose complex problems into manageable parts, choose the right structural patterns, and design software systems that are understandable, changeable, and resilient. This skill is language-agnostic — it is about thinking in systems rather than thinking in syntax.

Architecture is the set of decisions that are expensive to change later. Getting them right early — or at least making them reversible — is what separates codebases that thrive from codebases that calcify.

When To Use

  • Starting a new project or major feature and deciding how to organise code.
  • A system is growing beyond what a single file or module can sustain.
  • You need to separate concerns so that teams can work independently.
  • Choosing between libraries, frameworks, or infrastructure approaches.
  • Refactoring a tangled codebase into something maintainable.
  • Making decisions that will be hard or costly to reverse later.

Prerequisites

SkillWhy
Proficiency in at least one programming languageYou need to be able to think in abstractions beyond syntax
Basic understanding of data structuresChoosing the right data model is half the architecture

Instructions

1 — The Core Principle: Manage Complexity

Software architecture exists for one reason: to manage complexity so that humans can reason about the system.

Every technique below — from SOLID to hexagonal architecture — is a strategy for keeping the mental model small enough to hold in your head at any given time.

The two enemies of comprehension are:

  1. Coupling — when changing A forces you to change B, C, and D.
  2. Cognitive load — when understanding A requires understanding B, C, and D.

Good architecture minimises both.

2 — Foundational Principles

2.1 — Separation of Concerns

Every module, class, or function should have one reason to exist. When you read a piece of code, you should be able to answer: "What concern does this handle?" in one sentence.

code
BAD:  UserService handles authentication, profile CRUD, email sending, and billing.
GOOD: AuthService, ProfileService, EmailService, BillingService — each owns one concern.

2.2 — SOLID Principles

PrincipleOne-Sentence SummaryPractical Test
S — Single ResponsibilityA class has one reason to changeCan you describe what it does without using "and"?
O — Open/ClosedExtend behaviour without modifying existing codeCan you add a new variant without editing the switch/if-chain?
L — Liskov SubstitutionSubtypes must be usable wherever the base type is expectedDoes swapping the implementation break the caller's assumptions?
I — Interface SegregationClients shouldn't depend on methods they don't useDoes any implementor have a method that throws NotImplemented?
D — Dependency InversionDepend on abstractions, not concretionsCan you swap the database/API/filesystem without touching business logic?

2.3 — Composition Over Inheritance

Inheritance creates tight coupling (the fragile base class problem). Prefer composing behaviour from small, focused pieces:

python
# Inheritance approach (fragile)
class FlyingSwimmingAnimal(FlyingAnimal, SwimmingAnimal):  # Diamond problem
    pass

# Composition approach (flexible)
class Duck:
    def __init__(self):
        self.fly_behaviour = StandardFlight()
        self.swim_behaviour = FloatSwim()

    def fly(self):
        self.fly_behaviour.execute(self)

    def swim(self):
        self.swim_behaviour.execute(self)

Rule of thumb: Use inheritance for "is-a" relationships that are genuinely stable (they almost never are). Use composition for everything else.

2.4 — The Dependency Rule

Dependencies should always point inward — from less stable (UI, frameworks, I/O) toward more stable (business logic, domain model):

code
    ┌─────────────────────────────────────────┐
    │           Frameworks & Drivers           │  ← Most volatile
    │  ┌─────────────────────────────────────┐ │
    │  │        Interface Adapters           │ │
    │  │  ┌─────────────────────────────────┐│ │
    │  │  │        Application Logic        ││ │
    │  │  │  ┌─────────────────────────────┐││ │
    │  │  │  │      Domain / Entities      │││ │  ← Most stable
    │  │  │  └─────────────────────────────┘││ │
    │  │  └─────────────────────────────────┘│ │
    │  └─────────────────────────────────────┘ │
    └─────────────────────────────────────────┘

Inner layers never import from outer layers. Outer layers depend on inner layers through abstractions (interfaces/protocols).

3 — Common Architectural Patterns

3.1 — Layered Architecture

The most common starting point. Organise code into horizontal layers:

code
┌──────────────────┐
│   Presentation   │   UI, CLI, API controllers
├──────────────────┤
│    Application   │   Use cases, orchestration, DTOs
├──────────────────┤
│      Domain      │   Business rules, entities, value objects
├──────────────────┤
│  Infrastructure  │   Database, file system, external APIs, messaging
└──────────────────┘

Each layer only calls the layer directly below it. The domain layer has zero dependencies on infrastructure.

3.2 — Hexagonal Architecture (Ports & Adapters)

The domain sits at the centre. All external concerns connect through ports (interfaces defined by the domain) and adapters (implementations of those interfaces):

code
                    ┌────────────┐
        ┌──────────►│  REST API  │ (Driving adapter)
        │           └────────────┘
        │
   ┌────┴────┐      ┌──────────────────┐      ┌──────────────┐
   │  Port   │◄────►│   Domain Core    │◄────►│     Port     │
   │ (in)    │      │  (pure logic)    │      │    (out)     │
   └────┬────┘      └──────────────────┘      └──────┬───────┘
        │                                            │
        │           ┌────────────┐                   │
        └──────────►│    CLI     │    ┌──────────────┘
                    └────────────┘    │
                                ┌────┴──────────┐
                                │   PostgreSQL  │ (Driven adapter)
                                └───────────────┘

Why this matters: You can swap PostgreSQL for MongoDB, or a REST API for a CLI, without changing a single line of domain code. Testing also becomes trivial — inject in-memory adapters.

typescript
// Port (interface defined by the domain)
interface OrderRepository {
  save(order: Order): Promise<void>;
  findById(id: string): Promise<Order | null>;
}

// Adapter (infrastructure implements the port)
class PostgresOrderRepository implements OrderRepository {
  async save(order: Order): Promise<void> { /* SQL insert */ }
  async findById(id: string): Promise<Order | null> { /* SQL select */ }
}

// Test adapter
class InMemoryOrderRepository implements OrderRepository {
  private orders: Map<string, Order> = new Map();
  async save(order: Order) { this.orders.set(order.id, order); }
  async findById(id: string) { return this.orders.get(id) ?? null; }
}

3.3 — Event-Driven Architecture

Systems communicate by publishing and subscribing to events rather than calling each other directly. This decouples producers from consumers:

code
OrderService                          InventoryService
    │                                       │
    │── publishes ──► OrderPlaced ──────────►│ (subscribes)
    │                     │                  │── reduces stock
    │                     │
    │                     ▼
    │              NotificationService
    │                     │── sends confirmation email

When to reach for events:

  • Multiple systems need to react to the same thing.
  • You don't want the publisher to know about (or wait for) all consumers.
  • Operations can be eventually consistent rather than immediately consistent.

3.4 — CQRS (Command Query Responsibility Segregation)

Separate the write model (commands that change state) from the read model (queries that return data). Each can be optimised independently:

code
Commands (writes)                    Queries (reads)
┌──────────────┐                    ┌──────────────────┐
│ CreateOrder   │──► Write DB ──►  │ OrderSummaryView  │──► Read DB/Cache
│ CancelOrder   │   (normalised)   │ OrderDetailView   │   (denormalised)
│ UpdateAddress  │                  │ DashboardView     │
└──────────────┘                    └──────────────────┘

Use CQRS when: read and write loads differ dramatically, or the read shape doesn't match the write shape.

4 — Design Patterns That Matter Most

Out of the original 23 GoF patterns, these are the ones you'll use constantly:

PatternProblem It SolvesExample
StrategySwap algorithms at runtime without conditionalsPayment processing: CreditCard, PayPal, Crypto — each implements PaymentStrategy
Observer/Pub-SubNotify multiple objects of state changes without couplingUI reacting to model changes; event buses
FactoryCreate objects without specifying the exact classNotificationFactory.create("email") returns EmailNotification
RepositoryAbstract data access behind a collection-like interfaceuserRepository.findByEmail(email) — caller doesn't know if it's SQL, Mongo, or an API
DecoratorAdd behaviour to an object without modifying its classLoggingRepository(CachingRepository(SqlRepository())) — each wraps the next
AdapterMake incompatible interfaces work togetherWrapping a third-party API to fit your port interface
MediatorReduce many-to-many dependencies to many-to-oneA message bus / command dispatcher that routes requests

5 — Domain-Driven Design (DDD) Essentials

DDD is a set of techniques for modelling complex business domains. You don't need all of it, but these concepts are universally valuable:

ConceptWhat It Is
Ubiquitous LanguageThe whole team (devs, designers, stakeholders) uses the same terms in code and conversation. If the business says "Policy", the class is Policy — not InsuranceDocument.
Bounded ContextA boundary within which a term has a specific meaning. "Account" means different things in Billing vs. Authentication — they are separate bounded contexts.
EntityAn object with a unique identity that persists over time (User, Order).
Value ObjectAn object defined by its attributes, not identity. Two Money(100, "USD") are the same. Immutable.
AggregateA cluster of entities treated as a single unit for data changes. The aggregate root is the entry point (e.g., Order is the root; OrderLine is accessed through it).
Domain EventSomething that happened in the domain: OrderPlaced, PaymentReceived. Past tense. Immutable.

6 — Making Architectural Decisions

Architecture Decision Records (ADRs)

Document significant decisions in a lightweight format:

markdown
# ADR-003: Use PostgreSQL for primary data store

## Status
Accepted

## Context
We need a relational database that supports JSONB for flexible
schema extensions, has strong community support, and runs well
on our cloud provider.

## Decision
Use PostgreSQL 16+ as the primary data store, accessed through
the repository pattern so the domain has no direct SQL dependency.

## Consequences
- Team needs PostgreSQL expertise (mitigated: 3 of 5 devs have experience).
- We accept the operational cost of managing a relational DB.
- JSONB columns allow us to store semi-structured data without a separate document store.
- Switching to another DB later requires only rewriting the repository adapters.

Store ADRs in docs/decisions/ and number them sequentially. They are immutable — when you reverse a decision, write a new ADR that supersedes the old one.

Decision Criteria Checklist

Before committing to an architectural choice, ask:

  1. Reversibility — How expensive is it to change this decision later?
  2. Complexity budget — Does the added complexity pay for itself?
  3. Team capability — Can the team operate and debug this in production?
  4. Failure modes — What happens when this component fails?
  5. Scale trajectory — Will this hold for 10× users/data/traffic?

7 — System Design Thinking

Decomposition Strategies

StrategyQuestion It AnswersExample
By domainWhat business capabilities exist?Payments, Inventory, Shipping, Notifications
By volatilityWhat changes frequently vs. rarely?UI (fast) vs. core business rules (slow)
By teamWhat can a team own end-to-end?Team A owns search; Team B owns checkout
By dataWhat data belongs together?User profile data vs. analytics data
By riskWhat is safety-critical?Payment processing isolated from recommendations

Drawing System Diagrams

Use the C4 Model (Context, Containers, Components, Code) to communicate at the right level of abstraction:

LevelAudienceShows
1 — ContextEveryoneSystem boundary, users, external systems
2 — ContainerDevelopers + OpsApplications, databases, message queues
3 — ComponentDevelopersMajor structural blocks within a container
4 — CodeDeveloper working on itClasses, interfaces, relationships

Start at Level 1 and only zoom in when the audience needs it.

8 — API Design Principles

APIs are the contracts between components. A bad API is an architectural trap.

PrincipleExplanation
ConsistencyIf getUser() returns a User, then getOrder() should return an Order — not a dictionary, not a tuple
Least surpriseThe API should behave as a reasonable person would expect from its name
Small surface areaExpose the minimum needed. You can always add; removing is a breaking change
Fail explicitlyReturn typed errors or exceptions — never silently return null/empty when something went wrong
VersioningPlan for change from day one (URL versioning, header versioning, or semantic versioning for libraries)
IdempotencyOperations that can be safely retried (PUT, DELETE) should produce the same result on repeated calls

9 — When to Invest in Architecture

Not every project needs hexagonal architecture and DDD. The investment should match the project's lifespan and complexity:

Project TypeArchitectural Investment
Prototype / hackathonMinimal. Single file. Move fast. Throw it away.
Small tool (< 3 months)Layered separation. Tests for critical logic. Simple patterns.
Product (6+ months)Bounded contexts. Repository pattern. CI/CD. ADRs.
Platform (years, multiple teams)DDD. Hexagonal. Event-driven. API contracts. Formal documentation.

The biggest architectural mistake is premature complexity. The second biggest is no architecture at all. Both create systems that resist change.

Best Practices

  1. Defer decisions until the last responsible moment — the more you know, the better the decision. Use abstractions (interfaces) to keep options open.
  2. Optimise for changeability, not perfection — you will be wrong about some decisions. Make them cheap to reverse.
  3. Draw before you code — a 15-minute whiteboard session prevents weeks of rework. Use C4 diagrams.
  4. Make the implicit explicit — if a rule exists in the domain, it should exist as code in the domain layer, not as an IF statement scattered across the codebase.
  5. Write ADRs for every significant decision — future you (and your team) will thank you.
  6. Design for failure — every external dependency will fail. Plan for timeouts, retries, circuit breakers, and fallbacks.
  7. Limit the blast radius — isolate components so a failure in one doesn't cascading through the system.
  8. Apply YAGNI ruthlessly — don't build for hypothetical future requirements. Build what's needed now, but structure it so change is possible.
  9. Separate policy from mechanism — business rules (what to do) should be separate from infrastructure (how to do it).
  10. Review architecture in code review — structural decisions are more impactful than style choices. Prioritise review effort accordingly.

Common Pitfalls

PitfallHow It ManifestsFix
Big Ball of MudEverything depends on everything; no clear boundariesIntroduce boundaries gradually; start with the most volatile seam
Resume-Driven ArchitectureMicroservices, event sourcing, and Kubernetes for a CRUD appMatch complexity to the problem. Start simple, evolve as needed
Premature abstractionInterfaces with a single implementation "just in case"Wait for the second use case before abstracting. Three uses = pattern
God class / God moduleOne class/file that does everythingExtract responsibilities. Apply Single Responsibility Principle
Leaky abstractionsDatabase errors surfacing in the UI; HTTP concepts in the domainEnforce the dependency rule. Inner layers never know about outer layers
Distributed monolithMicroservices that must be deployed togetherIf services can't be deployed independently, they should be one service
Analysis paralysisSpending weeks choosing between patterns before writing codeSet a timebox. Make the decision reversible. Start with the simplest approach
Ignoring the domainTechnical organisation (controllers, services, models) instead of domain organisation (orders, payments, users)Group by feature / domain, not by technical role

Reference

  • Clean Architecture — Robert C. Martin (the dependency rule, use cases, boundaries)
  • A Philosophy of Software Design — John Ousterhout (complexity management, deep vs. shallow modules)
  • Domain-Driven Design — Eric Evans (bounded contexts, ubiquitous language, aggregates)
  • Designing Data-Intensive Applications — Martin Kleppmann (data architecture, event sourcing, CQRS)
  • C4 Model — Simon Brown (system diagramming)
  • ADR GitHub Org — Architecture Decision Record templates and tooling
  • The Twelve-Factor App — Methodology for building modern, deployable applications