agentleFS
mattstrublemattstruble/skills/application-architecture/SKILL.md

application-architecture

Skill2 starsChanged 4 months agoMITView on GitHub

Application-level structural patterns -- layering, business logic organization, data access strategy, domain modeling, and cross-boundary communication. Use when designing a new service, choosing how to structure an application, or reviewing code for architectural smells (logic in controllers, no domain boundary, tangled data access). NOT for code-level design (see software-design), API surface conventions (see api-design), or infrastructure/deployment architecture.

The file

We split this skill into the rules it gives an agent

6 rules. About 0 are common, 6 are distinctive, and 0 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈6

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 0

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Every decision tree below starts with a YAGNI gate.

    line 13
  2. Distinctive:

    Read the relevant reference file when you need deeper trade-off analysis, criteria, and pseudocode examples.

    line 17
  3. Distinctive:

    When reviewing existing code, use this table to identify which decision point is relevant.

    line 24
  4. Distinctive:

    All data is flat, with no invariants to enforce and no behavior beyond CRUD?

    line 117
  5. Distinctive:

    Use plain data structures (structs, dicts, dataclasses).

    line 118
  6. Distinctive:

    Substrate note: when a layer spawns endless workaround layers (framework churn, adapter chains), the defect is usually the layer beneath.

    line 232

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Application Architecture

Default to the simplest structure that works. Add architectural patterns only when complexity demands them -- not before.

Every decision tree below starts with a YAGNI gate. If the gate tells you to stop, stop. Patterns exist to manage complexity; applying them to simple systems creates the complexity they were designed to solve.

Read the relevant reference file when you need deeper trade-off analysis, criteria, and pseudocode examples.


Architectural Smell Table

When reviewing existing code, use this table to identify which decision point is relevant. A smell is not always a problem -- check the YAGNI gate before recommending changes.

SmellLikely ProblemDecision Point
Business logic in controllers/route handlersMissing domain layerBusiness Logic Structure
God service that reads, writes, validates, and transformsTransaction Script outgrowing its contextBusiness Logic Structure
Domain objects know how to save themselvesActive Record in a complex domainData Access Strategy
Database queries scattered across business logicNo data access boundaryData Access Strategy
Raw primitives everywhere (strings for money, ints for dates)Missing Value ObjectsDomain Modeling
Null checks throughout the codebaseMissing Special Case objectsDomain Modeling
Internal service calls mirror the database schemaNo translation at boundaryCross-Boundary Communication
Downstream changes break upstream codeMissing anti-corruption layerCross-Boundary Communication
Cascading side effects on every writeImplicit coupling through state mutationState Changes & Side Effects
Audit trail bolted on after the factMissing event-driven state trackingState Changes & Side Effects

1. Business Logic Structure

The question: Where does business logic live, and how is it organized?

YAGNI Gate

Simple CRUD service with <5 business rules and minimal conditional logic? -> Transaction Script (procedural functions, one per operation). Stop here. No domain model, no service layer. Add structure when the transaction scripts start duplicating logic or growing beyond what one developer can hold in their head.

Decision Criteria

ChooseWhenWatch for
Transaction ScriptSimple operations, few business rules, logic fits in one function per use caseDuplication across scripts signals time to extract a Domain Model
Domain ModelComplex business rules, multiple entities interacting, rules that change independentlyOver-modeling simple CRUD; building an object graph when a function would do
Service LayerMultiple entry points (API, CLI, queue) need the same operations; transaction coordination neededAnemic domain: service does all work while domain model is just a data bag
Hexagonal ArchitectureDomain logic must be tested independently of infrastructure; multiple adapters swap in/outPremature port/adapter abstraction when you have one adapter and no plans for more

Progression: Most applications start as Transaction Script. When scripts duplicate logic or conditionals multiply, extract a Domain Model. When multiple interfaces need the same operations, add a Service Layer. When infrastructure coupling becomes painful, consider Hexagonal Architecture.

Implementation: See software-design for composition and interface design. See python-design for Protocol-based port definitions.

-> Read references/business-logic-structure.md for detailed trade-offs and pseudocode.


2. Data Access Strategy

The question: How does the application read and write persistent data?

YAGNI Gate

Using an ORM with straightforward queries against <10 tables? -> Use the ORM directly. No Repository, no Gateway abstraction. The ORM is your data access layer. Add abstractions when you need to test domain logic without a database, or when query logic starts duplicating across the codebase.

Decision Criteria

ChooseWhenWatch for
Direct ORM / Query BuilderSimple data access, schema maps closely to domain, few complex queriesSpreading query logic across business code
Active RecordDomain objects map 1:1 to database rows, limited business logic per entityDomain complexity growing beyond what the record pattern supports
RepositoryComplex domain model, need to test in isolation, query logic duplicatingRepository becoming a thin pass-through that adds indirection without value
Data MapperDomain schema diverges from database schema, need full decouplingOver-engineering when schemas match; most ORMs already are Data Mappers
GatewayWrapping access to a specific external resource (API, file system, message queue)Building a Gateway for a dependency you'll never swap
Table Data GatewayReporting queries, bulk operations, set-based logic that doesn't map to domain objectsMixing procedural table access with OO domain access inconsistently

Key distinction: Repository abstracts collections of domain objects. Gateway abstracts access to a specific resource. A Repository contains domain logic (query specifications, consistency rules); a Gateway is a thin wrapper over an external system.

Implementation: See python-design for Protocol-based repository interfaces. See api-design for Gateway API surface design.

-> Read references/data-access-strategy.md for detailed trade-offs and pseudocode.


3. Domain Modeling

The question: How should domain concepts be represented in code?

YAGNI Gate

All data is flat, with no invariants to enforce and no behavior beyond CRUD? -> Use plain data structures (structs, dicts, dataclasses). No Value Objects, no Aggregates. Domain modeling adds value when there are business rules that data structures alone can't enforce.

Decision Criteria

ConceptUse whenKey property
Value ObjectConcept defined by its attributes, not its identity (Money, DateRange, EmailAddress)Immutable, equality by value, self-validating
EntityConcept with a lifecycle and identity that persists across state changes (User, Order)Mutable state, equality by identity (ID), encapsulates business rules
AggregateCluster of entities/value objects with a consistency boundary -- changes go through the rootTransactional consistency: invariants hold after every operation on the root
Special CaseSpecific subtype that eliminates null checks (NullCustomer, MissingOrder)Polymorphic with the normal case; callers don't branch on null

Aggregate boundaries: The most common mistake is making aggregates too large. An Aggregate is a transactional consistency boundary, not a containment hierarchy. Ask: "What data must be consistent within a single transaction?" That's your aggregate. Everything else references by ID, not by direct object reference.

Implementation: See python-design for frozen dataclass (Value Object), Protocol (entity interfaces), and Enum (Special Case) patterns.

-> Read references/domain-modeling.md for detailed trade-offs and pseudocode.


4. Cross-Boundary Communication

The question: How does data cross process, service, or team boundaries?

YAGNI Gate

Single-process monolith where all code is in the same deployment? -> Call functions directly. No DTOs, no facades, no anti-corruption layers. These patterns solve problems that arise from distribution, not from complexity.

Decision Criteria

ChooseWhenWatch for
DTOData crosses a process/network boundary and the internal model shouldn't leakDTOs that mirror internal models 1:1 -- if they're identical, you don't need a separate type
Remote FacadeFine-grained internal API needs a coarse-grained external interface to reduce network callsFacade becoming a God object that orchestrates too much; logic should stay in the domain
Anti-Corruption LayerIntegrating with an external system whose model doesn't match yoursBuilding an ACL for a system you control -- fix the source instead

DTO direction: DTOs serve the consumer, not the producer. Design them around what the caller needs, not what the database returns. A DTO that mirrors your ORM model is not a DTO -- it's a leak.

Implementation: See api-design for field naming, pagination, and error handling in DTOs. See software-design for Separated Interface patterns at boundaries.

-> Read references/cross-boundary-communication.md for detailed trade-offs and pseudocode.


5. State Changes and Side Effects

The question: How should the system handle writes, side effects, and derived actions?

YAGNI Gate

Simple request-response where each write hits one table and triggers no downstream effects? -> Let your ORM handle transactions directly. No events, no CQRS, no event sourcing. These patterns exist for systems where a single write triggers multiple independent reactions or where the read model diverges from the write model.

Decision Criteria

ChooseWhenWatch for
Unit of WorkMultiple changes must commit or fail atomically; tracking dirty objectsMost ORMs implement this already -- don't reimplement what your framework provides
Domain EventsState change should trigger independent side effects (email, cache, notify downstream)Events used for synchronous in-process calls that should just be function calls
CQRSRead and write models have fundamentally different shapes or performance requirementsApplying CQRS when reads and writes use the same model -- adds two models and sync complexity for no benefit
Event SourcingComplete audit trail required, need to reconstruct state at any point, or domain fits naturally as event sequenceEnormous complexity cost; only justified when the event log is the domain model

Escalation ladder: Direct ORM transactions -> Unit of Work (if ORM doesn't cover it) -> Domain Events (when side effects appear) -> CQRS (when read/write models diverge) -> Event Sourcing (when event log is source of truth). Each step adds significant complexity. Justify every escalation.

Implementation: See software-design for pure functions (keeping side effects at boundaries). See test-design for testing event-driven architectures.

-> Read references/state-changes-side-effects.md for detailed trade-offs and pseudocode.


6. Concurrency, Load & Infrastructure Realism

The question: How should the application handle threads, async I/O, load spikes, and infrastructure choices?

YAGNI Gate

Single-threaded, low-traffic service with no blocking I/O contention and no infrastructure complexity? -> Do nothing here. Concurrency primitives, async runtimes, and distributed infrastructure all carry real costs. Add them only when a measured problem demands them.

Decision Criteria

SituationPreferred approachWatch for
Blocking I/O (DB, HTTP)Thread pool sized to core count, or explicit task-state + scheduler loopSpinning a thread per task; treating coroutines as a free abstraction (see reference for trade-offs)
Shared mutable stateMinimal lock scope; assertable ownership; static lock-order priorityCode under a lock that acquires other locks; complex logic inside a critical section
Load spikesModel realistic concurrency; over-provision for launch; degrade gracefullyAssuming utilization headroom is linear — wait times blow up non-linearly as capacity is approached
Infrastructure choicesUse the simplest stack the problem actually requiresImporting Docker/K8s/microservices because "that's how web services work" when the problem doesn't demand it
Async non-determinismKeep async at the user level; prefer serial code or threadsAsync baked into the architecture — its failure mode is wrong answers, not crashes

Substrate note: when a layer spawns endless workaround layers (framework churn, adapter chains), the defect is usually the layer beneath. Fix the source. This extends the Anti-Corruption Layer guidance in §4: an ACL is appropriate for external systems you don't control; for systems you do control, fix the source instead.

Events note: for sequential actions, write serial code or use a thread. For reactive patterns, decide exactly when in the execution cycle the handler fires before reaching for an event system. This reinforces the Domain Events caution in §5.

-> Read references/concurrency-and-load.md for thread/async trade-offs, mutex discipline, queueing theory, and infrastructure minimalism.


References

ReferenceWhen to read
references/business-logic-structure.mdChoosing between Transaction Script, Domain Model, Service Layer, and Hexagonal Architecture. Detailed criteria, trade-offs, and pseudocode.
references/data-access-strategy.mdChoosing between direct ORM, Active Record, Repository, Data Mapper, and Gateway. When abstractions help vs. when they add indirection.
references/domain-modeling.mdDesigning Value Objects, Entities, Aggregates, and Special Case objects. Aggregate boundary rules and common mistakes.
references/cross-boundary-communication.mdDTOs, Remote Facade, and Anti-Corruption Layer. How data should cross service and process boundaries.
references/state-changes-side-effects.mdUnit of Work, Domain Events, CQRS, and Event Sourcing. When to escalate from simple transactions to event-driven patterns.
references/concurrency-and-load.mdThread/async trade-offs, mutex discipline, queueing theory, and infrastructure minimalism. When concurrency primitives and distributed infrastructure are and aren't warranted.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.