AgentSkillsCN

Software Architecture

软件架构

SKILL.md

Software Architecture

Foundational principles and patterns for maintainable software systems.


1. Domain-Driven Design (DDD)

"An approach to software development that centers the development on programming a domain model that has a rich understanding of the processes and rules of a domain." — Eric Evans

Core insight: Model the business domain, not the database. The domain is the problem space; the model is your solution.

Ubiquitous Language

A shared vocabulary between developers and domain experts, embedded directly in code.

PrincipleApplication
Same terms in code and conversationShootout, not ToneComparison or comparison_table
Evolve language with understandingRename when domain knowledge deepens
Reject technical jargon in domainSignalChain, not AudioProcessingPipeline

Bounded Contexts

A boundary within which a domain model applies. Large systems have multiple contexts, each with its own model.

code
┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐
│   Shootouts     │  │  Signal Chains  │  │   Gear Catalog  │
│                 │  │                 │  │                 │
│  Shootout       │  │  SignalChain    │  │  Gear           │
│  ToneSelection  │  │  SignalBlock    │  │  GearModel      │
│                 │  │  ChainGroup     │  │  GearTag        │
└─────────────────┘  └─────────────────┘  └─────────────────┘
       │                    │                    │
       └────────────────────┼────────────────────┘
                            │
                    Context Mapping
                  (how contexts relate)

Key rule: Aggregates in different contexts should not directly reference each other. Use IDs or domain events.

Tactical Patterns

PatternDefinitionExample
EntityObject with identity that persists across state changesUser (same user even if name changes)
Value ObjectImmutable object defined by its attributesToneSelection (amp_id + cab_id)
AggregateCluster of entities/value objects with a root that owns consistencyShootout owns its ToneSelections
RepositoryAbstraction for retrieving and storing aggregatesShootoutRepository.get_by_id()
Domain EventRecord of something significant that happenedShootoutCreated, ToneSyncCompleted
Domain ServiceStateless operation that doesn't belong to an entityGearMatchingService.find_similar()

Aggregate Rules

  1. Single root — Only the aggregate root is referenced externally
  2. Consistency boundary — Invariants enforced within the aggregate
  3. Transactional boundary — One aggregate per transaction
  4. Identity equality — Aggregates compared by ID, not attributes
python
# Aggregate root with identity equality
@dataclass
class Shootout:
    id: UUID
    title: str
    selections: list[ToneSelection]  # Owned by this aggregate

    def __eq__(self, other: object) -> bool:
        return isinstance(other, Shootout) and self.id == other.id

    def __hash__(self) -> int:
        return hash(self.id)

2. CQRS (Command Query Responsibility Segregation)

"Use a different model to update information than the model you use to read information." — Martin Fowler

Core insight: Reads and writes have different needs. Separating them allows optimisation of each.

code
┌──────────────┐         ┌──────────────┐
│   Command    │         │    Query     │
│    Model     │         │    Model     │
│              │         │              │
│  Optimised   │         │  Optimised   │
│  for writes  │         │  for reads   │
└──────┬───────┘         └──────┬───────┘
       │                        │
       │    ┌──────────┐        │
       └───►│   Event  │◄───────┘
            │   Store  │
            └──────────┘

When to Apply CQRS

Apply WhenAvoid When
Read and write models diverge significantlySimple CRUD operations
Different scaling needs for reads vs writesLow complexity domain
Complex reporting requirementsSmall team, rapid iteration
Event sourcing is beneficialConsistency requirements are simple

Important: CQRS applies to specific bounded contexts, not entire systems.

Relationship to Event Sourcing

CQRS pairs naturally with event-based architectures:

  • Write model produces events
  • Read models consume events as projections
  • Enables temporal queries ("state at time T")

3. Hexagonal Architecture (Ports and Adapters)

"Allow an application to equally be driven by users, programs, automated test or batch scripts, and to be developed and tested in isolation from its eventual run-time devices and databases." — Alistair Cockburn

Core insight: Isolate domain logic from infrastructure. The application has an inside (domain) and outside (infrastructure), connected through ports.

The Hexagon

code
                    Primary Adapters
                   (drive the application)
                          │
            ┌─────────────┼─────────────┐
            │             ▼             │
            │   ┌─────────────────┐     │
   HTTP ────┼──►│                 │     │
   API      │   │     DOMAIN      │     │
            │   │                 │◄────┼──── Database
   CLI ─────┼──►│   (pure logic)  │     │     Adapter
            │   │                 │◄────┼──── External API
   Tests ───┼──►│                 │     │     Adapter
            │   └─────────────────┘     │
            │             ▲             │
            └─────────────┼─────────────┘
                          │
                  Secondary Adapters
                (driven by the application)

Ports and Adapters

ConceptDefinitionExample
PortInterface defining how to interact with domainShootoutRepository protocol
AdapterImplementation connecting port to infrastructureSQLAlchemyShootoutRepository
Primary PortHow external actors drive the applicationAPI endpoints, CLI commands
Secondary PortHow the application drives external systemsDatabase, external APIs

The Critical Rule: Inside vs Outside

code
INSIDE (Domain)              OUTSIDE (Infrastructure)
────────────────             ────────────────────────
- Business rules             - HTTP frameworks
- Entities                   - ORMs
- Value objects              - External APIs
- Domain services            - Message queues
- Port definitions           - File systems

        │
        │  Ports define the boundary
        │  Adapters implement the connection
        ▼

Import direction: Outside depends on Inside. Never the reverse.


4. Pythonic Implementation

Following Cosmic Python patterns.

Ports: Protocol vs ABC

Prefer Protocol (structural typing) for most cases:

python
# ports/repository.py
from typing import Protocol
from domain.entities import Shootout

class ShootoutRepository(Protocol):
    """Port for shootout persistence."""

    async def get_by_id(self, id: UUID) -> Shootout | None:
        """Retrieve shootout by ID."""
        ...

    async def save(self, shootout: Shootout) -> None:
        """Persist shootout."""
        ...

Use ABC when you need runtime enforcement or shared implementation:

python
from abc import ABC, abstractmethod

class OAuthProvider(ABC):
    """Base for OAuth providers with shared token refresh logic."""

    @abstractmethod
    async def get_user_info(self, token: str) -> UserInfo:
        pass

    async def refresh_if_needed(self, tokens: OAuthTokens) -> OAuthTokens:
        """Shared implementation for all providers."""
        if tokens.is_expired:
            return await self._refresh(tokens)
        return tokens

Adapters: One per Infrastructure Concern

python
# adapters/persistence/shootout_repository.py
from domain.ports import ShootoutRepository
from domain.entities import Shootout

class SQLAlchemyShootoutRepository:
    """Adapter: implements ShootoutRepository port with SQLAlchemy."""

    def __init__(self, session: AsyncSession):
        self._session = session

    async def get_by_id(self, id: UUID) -> Shootout | None:
        stmt = select(ShootoutModel).where(ShootoutModel.id == id)
        result = await self._session.execute(stmt)
        model = result.scalar_one_or_none()
        return ShootoutMapper.to_entity(model) if model else None

    async def save(self, shootout: Shootout) -> None:
        model = ShootoutMapper.to_model(shootout)
        self._session.add(model)
        await self._session.flush()

Repository Pattern

"A simplifying abstraction over data storage, allowing us to decouple our model layer from the data layer." — Cosmic Python

python
# The illusion of an in-memory collection
class AbstractRepository(Protocol[T]):
    """Minimal interface: add and get."""

    async def add(self, entity: T) -> None: ...
    async def get(self, id: UUID) -> T | None: ...

# Fake for testing (trivial to implement)
class FakeShootoutRepository:
    def __init__(self):
        self._shootouts: dict[UUID, Shootout] = {}

    async def add(self, shootout: Shootout) -> None:
        self._shootouts[shootout.id] = shootout

    async def get(self, id: UUID) -> Shootout | None:
        return self._shootouts.get(id)

Unit of Work Pattern

"An abstraction over atomic operations... either all changes succeed or none persist." — Cosmic Python

python
# Using Python's context manager protocol
class UnitOfWork:
    def __init__(self, session_factory):
        self._session_factory = session_factory

    async def __aenter__(self):
        self._session = self._session_factory()
        self.shootouts = SQLAlchemyShootoutRepository(self._session)
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        if exc_type:
            await self._session.rollback()
        await self._session.close()

    async def commit(self):
        await self._session.commit()

# Usage in service
async def create_shootout(self, data: CreateShootout) -> Shootout:
    async with self._uow:
        shootout = Shootout.create(data)
        await self._uow.shootouts.add(shootout)
        await self._uow.commit()
        return shootout

Service Layer with Dependency Injection

python
# services/shootout_service.py
class ShootoutService:
    """Orchestrates domain logic. Depends on ports, not adapters."""

    def __init__(
        self,
        repo: ShootoutRepository,      # Port (Protocol)
        job_queue: JobQueuePort,       # Port (Protocol)
    ):
        self._repo = repo
        self._job_queue = job_queue

    async def create_shootout(
        self,
        user_id: UUID,
        data: CreateShootoutData,
    ) -> Shootout:
        """Domain orchestration - no infrastructure knowledge."""
        shootout = Shootout.create(user_id=user_id, title=data.title)

        for tone in data.tones:
            shootout.add_tone(tone)  # Domain logic in entity

        await self._repo.save(shootout)
        await self._job_queue.enqueue(RenderShootoutJob(shootout.id))

        return shootout

Composition Root (Wiring)

python
# bootstrap.py or api/deps.py
def create_shootout_service(session: AsyncSession) -> ShootoutService:
    """Wire adapters to ports at the composition root."""
    return ShootoutService(
        repo=SQLAlchemyShootoutRepository(session),
        job_queue=TaskIQJobQueue(broker),
    )

# FastAPI dependency
async def get_shootout_service(
    session: AsyncSession = Depends(get_session),
) -> ShootoutService:
    return create_shootout_service(session)

5. SOLID Principles

PrincipleDefinitionVerification
Single ResponsibilityOne reason to changeEach adapter handles one provider/concern
Open/ClosedOpen for extension, closed for modificationNew adapter = new file, no changes to service
Liskov SubstitutionSubtypes must be substitutableAll adapters return same domain types
Interface SegregationClients shouldn't depend on unused methodsPorts have only methods they need
Dependency InversionDepend on abstractions, not concretionsServices depend on Ports, not Adapters

Verification Commands

bash
# Single Responsibility: Check adapter file count per concern
ls -la backend/app/adapters/persistence/
ls -la backend/app/adapters/external/

# Open/Closed: Adding new adapter shouldn't touch services
git diff --name-only HEAD~5 | grep -E "adapters.*\.py$"

# Dependency Inversion: Services import from ports, not adapters
grep -r "from app.adapters" backend/app/services/
# Should return empty (violation if not)

6. Layer Dependencies

Import Rules

code
┌─────────────────────────────────────────────────────────┐
│                         API Layer                        │
│            (routes, schemas, dependencies)              │
└───────────────────────────┬─────────────────────────────┘
                            │ imports
                            ▼
┌─────────────────────────────────────────────────────────┐
│                      Service Layer                       │
│              (orchestration, transactions)              │
└───────────────────────────┬─────────────────────────────┘
                            │ imports
              ┌─────────────┴─────────────┐
              ▼                           ▼
┌─────────────────────────┐   ┌─────────────────────────┐
│      Adapter Layer      │   │      Domain Layer       │
│   (implementations)     │   │   (entities, ports)     │
└─────────────────────────┘   └─────────────────────────┘
              │                           ▲
              │         imports           │
              └───────────────────────────┘
LayerCan ImportCannot Import
DomainNothing (pure)Services, Adapters, API, Models
AdaptersDomainServices, API
ServicesDomain, Adapters (via DI)API
APIServices, Schemas, Domain typesAdapters directly

Verification

bash
# Domain must not import infrastructure
grep -r "from app.services" backend/app/domain/  # Should be empty
grep -r "from app.adapters" backend/app/domain/  # Should be empty
grep -r "from app.models" backend/app/domain/    # Should be empty

# Adapters must not import services
grep -r "from app.services" backend/app/adapters/  # Should be empty

7. Directory Structure

code
backend/app/
├── domain/                    # INSIDE (pure Python)
│   ├── entities/              # Aggregate roots and entities
│   │   ├── shootout.py        # Shootout aggregate
│   │   ├── signal_chain.py    # SignalChain aggregate
│   │   └── user.py            # User aggregate
│   ├── value_objects/         # Immutable domain concepts
│   │   └── tone_selection.py
│   ├── ports/                 # Interface definitions (Protocols)
│   │   └── repositories.py    # Repository protocols
│   ├── events/                # Domain events
│   └── exceptions.py          # Domain exceptions
│
├── services/                  # Application/Use Case layer
│   ├── shootout_service.py    # Shootout orchestration
│   └── auth_service.py        # Auth orchestration
│
├── adapters/                  # OUTSIDE (infrastructure)
│   ├── persistence/           # Database adapters
│   │   ├── shootout_repo.py   # SQLAlchemy implementation
│   │   └── mappers.py         # ORM ↔ Entity conversion
│   ├── external/              # External API adapters
│   │   └── tone3000.py        # T3K API client
│   └── processing/            # Processing adapters
│       └── ffmpeg.py          # Video processing
│
├── api/                       # Primary adapters (HTTP)
│   └── v1/
│       ├── shootouts.py       # Shootout endpoints
│       └── deps.py            # Dependency injection
│
└── models/                    # ORM models (infrastructure)
    └── shootout.py            # SQLAlchemy model

8. Anti-Patterns

Anti-PatternProblemCorrect Pattern
Domain imports ORMCouples domain to databaseUse mappers in adapters
Service creates adapterService knows infrastructureInject via constructor
Adapter calls another adapterAdapters become coupledCoordinate in service layer
Aggregate references another aggregateViolates consistency boundaryUse IDs, domain events
Business logic in API handlerLogic scattered, untestableMove to service/entity
Repository returns ORM modelLeaks infrastructure to domainReturn domain entity

9. Testing Strategy

Test Pyramid with Hex Architecture

code
                    ┌───────────┐
                    │   E2E     │  Few: Full system via HTTP
                    ├───────────┤
                    │Integration│  Some: Service + real adapters
                    ├───────────┤
                    │   Unit    │  Many: Domain + fake adapters
                    └───────────┘

Testing Each Layer

LayerTest TypeApproach
DomainUnitPure Python, no mocks needed
ServicesUnitInject fake adapters
AdaptersIntegrationReal infrastructure (DB, APIs)
APIIntegrationReal service, real/fake adapters
Full SystemE2EHTTP requests, real everything
python
# Unit test with fake adapter
async def test_create_shootout():
    fake_repo = FakeShootoutRepository()
    fake_queue = FakeJobQueue()
    service = ShootoutService(repo=fake_repo, job_queue=fake_queue)

    shootout = await service.create_shootout(user_id=UUID(...), data=...)

    assert await fake_repo.get(shootout.id) is not None
    assert len(fake_queue.jobs) == 1

References


GTS-Specific

For GTS implementation details:

  • Aggregates & Contexts: See DEVELOPMENT.md Section "Domain Architecture"
  • Current Structure: See .planning/codebase/ARCHITECTURE.md
  • Verification: See /codebase-review --section=architecture