agentleFS
Khuirul-HudaKhuirul-Huda/clean-principles/skills/architecture-principles/SKILL.md

architecture-principles

Skill0 starsChanged 30 days agoMITView on GitHub

Use when designing system structure, splitting modules/services, or when code changes in one place unexpectedly break another.

3 other projects use a version of this.

Adopted by

Most recently taken up by

The file

We split this skill into the rules it gives an agent

1 rule. 0 are common, 1 is distinctive, and 0 are very specific to this project.

  • 0

    Common

    Most of the 3 edited versions we read keep these.

  • 1

    Distinctive

    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:

    Benefits: Read and write models evolve independently.

    line 92

“Common” is measured: kept by at least 6 in 10 of the 3 edited versions of this file we could read. “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

Architecture Principles

Principles for structuring systems so they're easy to understand, change, and scale.

Separation of Concerns (SoC)

Each module has one clearly defined responsibility. Concerns don't bleed into each other.

❌ BAD — Everything in one layer:
  route handler → validates input → runs business logic → queries DB → formats response

✅ GOOD — Each layer has one job:
  Controller  → validates HTTP input, delegates
  Service     → business logic, orchestrates
  Repository  → data access only
  Serializer  → formats output

Boundary test: Can you change the database without touching business logic? Can you change the API format without touching domain logic? If no → concerns are mixed.


Layered Architecture

Dependencies flow one way: outer layers depend on inner layers, never the reverse.

┌─────────────────────┐
│  Presentation       │  HTTP, CLI, WebSocket
├─────────────────────┤
│  Application        │  Use cases, orchestration
├─────────────────────┤
│  Domain             │  Business rules, entities  ← Core, no dependencies
├─────────────────────┤
│  Infrastructure     │  DB, email, queues, APIs
└─────────────────────┘

Rule: Domain knows NOTHING about Infrastructure.
      Infrastructure implements Domain interfaces.

Hexagonal Architecture (Ports & Adapters)

Core business logic is isolated. External systems plug in via adapters.

         [ HTTP API ]  [ CLI ]  [ Tests ]
               ↓          ↓        ↓
          ┌────────────────────────┐
          │      Application       │ ← Ports (interfaces)
          │    (Business Logic)    │
          └────────────────────────┘
               ↓          ↓        ↓
         [ PostgreSQL ] [ Redis ] [ SendGrid ]
                     (Adapters)

Benefit: Swap any external system (DB, email provider) without changing business logic. Business logic is testable with no infrastructure.


CQRS — Command Query Responsibility Segregation

Separate write operations (Commands) from read operations (Queries).

# ❌ BAD: Mixed reads and writes — hard to optimize independently
class UserService:
    def create_user(self, data): ...       # command
    def update_email(self, id, email): ... # command
    def find_by_id(self, id): ...          # query
    def search_users(self, query): ...     # query — often needs different optimization

# ✅ GOOD: Separated by intent
class UserCommandService:
    def create(self, data: CreateUserDTO) -> None: ...
    def update_email(self, id: int, email: str) -> None: ...

class UserQueryService:
    def find_by_id(self, id: int) -> UserView: ...
    def search(self, query: str) -> list[UserView]: ...

Benefits: Read and write models evolve independently. Query side can use caching, denormalized views, or read replicas without affecting write logic.

Note: Full CQRS with separate databases is advanced. Even splitting command/query into separate classes in the same service is a meaningful step.


Event-Driven Architecture

Modules communicate by publishing events — not by calling each other directly.

# ❌ BAD: OrderService knows about every downstream service
class OrderService:
    def place_order(self, order):
        self.db.save(order)
        self.email_service.send_confirmation(order)  # tight coupling
        self.inventory_service.reserve(order.items)
        self.billing_service.charge(order)

# ✅ GOOD: OrderService publishes one event — knows nothing downstream
class OrderService:
    def place_order(self, order):
        self.db.save(order)
        self.event_bus.publish("order.placed", order)  # zero coupling

# Each service subscribes independently:
email_service.on("order.placed", send_confirmation)
inventory_service.on("order.placed", reserve_items)
billing_service.on("order.placed", charge_customer)
# Adding analytics? Zero changes to OrderService.

Benefit: Adding a new reaction requires zero changes to the publisher.

Trade-off: Flow is harder to trace — invest in structured logging and observability.


High Cohesion, Low Coupling

ConceptGoalSmell
High CohesionThings that change together, live togetherFeature spread across 10 files
Low CouplingModules communicate through narrow interfacesOne module importing 20 things from another
# ❌ HIGH COUPLING: Order module reaches into User internals
class OrderService:
    def place(self, user_id):
        user = UserRepository().find(user_id)
        address = user._private_address_data["street"]  # Reaching in!

# ✅ LOW COUPLING: Order asks for what it needs through interface
class OrderService:
    def place(self, user_id):
        address = self.user_service.get_shipping_address(user_id)  # Clean contract

Law of Demeter (Don't Talk to Strangers)

A method should only call methods on: itself, its parameters, objects it creates, its direct fields.

# ❌ BAD: Chain of knowledge (fragile)
total = order.get_customer().get_wallet().get_balance()

# ✅ GOOD: Ask the direct collaborator
total = order.get_customer_balance()  # Customer encapsulates wallet

Package / Module Organization

Two valid strategies — pick one, be consistent:

StrategyStructureBest for
By Layercontrollers/, services/, repos/Small teams, simple CRUD apps
By Featureorders/, users/, billing/Growing apps, team ownership

By feature scales better — all order code lives in orders/, changes are contained.


Quick Architecture Checklist

  • Can I describe each module's responsibility in one sentence?
  • Does the domain layer have zero imports from infrastructure?
  • Can I test business logic without a database or HTTP server?
  • Does changing a database table require changes outside the repository?
  • Are module boundaries obvious from the folder structure?
  • Are commands (writes) separated from queries (reads)?
  • Do modules communicate through events rather than direct calls where decoupling matters?

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.