home / skills / martinffx / claude-code-atelier / atelier-python-architecture

This skill guides Python application architecture with functional core and domain-driven design, promoting clean boundaries, robust data modeling, and testable

npx playbooks add skill martinffx/claude-code-atelier --skill atelier-python-architecture

Review the files below or copy the command above to add this skill to your agents.

Files (4)
SKILL.md
5.1 KB
---
name: python:architecture
description: Python application architecture with functional core, effectful shell, DDD, and data modeling. Use when designing application layers, separating pure business logic from IO, defining domain models, implementing validation, or structuring bounded contexts.
user-invocable: false
---

# Python Application Architecture

Modern Python application architecture following functional core / imperative shell pattern, Domain-Driven Design, and type-safe data modeling.

## Core Principle: Functional Core / Imperative Shell

Separate pure business logic from side effects:

- **Functional Core**: Pure functions, business logic, no IO
- **Imperative Shell**: Coordinates external dependencies, handles side effects

See [references/functional-core.md](references/functional-core.md) for detailed patterns and examples.

## Layered Architecture

Follow bottom-up dependency flow:

```
Router/Handler → Service → Repository → Entity → Database
```

Each layer depends only on layers below.

**Responsibilities:**
- **Entity**: Domain models, validation, business rules, data transformations (fromRequest, toRecord, toResponse)
- **Repository**: Abstract storage interface, returns domain entities
- **Service**: Business workflows, orchestrates entities and repositories
- **Router/Handler**: HTTP handling, delegates to services

## Domain Models

### Entity Example

```python
from dataclasses import dataclass
from uuid import UUID
from decimal import Decimal

@dataclass
class Order:
    """Entity - has identity and encapsulated behavior"""
    id: UUID
    customer_id: UUID
    total: Decimal
    status: str

    def apply_discount(self, rate: Decimal) -> None:
        """Business rule - encapsulated in entity"""
        if self.status == "pending":
            self.total = self.total * (1 - rate)

    @classmethod
    def from_request(cls, req, customer_id: UUID) -> "Order":
        """Transform API request → entity"""
        return cls(id=uuid4(), customer_id=customer_id, total=Decimal("0"), status="pending")

    def to_response(self):
        """Transform entity → API response"""
        return {"id": self.id, "total": self.total, "status": self.status}
```

### Value Object Example

```python
from dataclasses import dataclass

@dataclass(frozen=True)
class Money:
    """Value object - immutable, no identity"""
    amount: Decimal
    currency: str

    def add(self, other: "Money") -> "Money":
        if self.currency != other.currency:
            raise ValueError("Cannot add different currencies")
        return Money(self.amount + other.amount, self.currency)
```

See [references/ddd.md](references/ddd.md) for aggregates, bounded contexts, and domain services.

## Repository Pattern

Abstract storage behind interface:

```python
from abc import ABC, abstractmethod
from typing import Optional

class OrderRepository(ABC):
    """Abstract repository - interface only"""

    @abstractmethod
    def get(self, order_id: UUID) -> Optional[Order]:
        pass

    @abstractmethod
    def save(self, order: Order) -> None:
        pass

class PostgresOrderRepository(OrderRepository):
    """Concrete implementation"""

    def get(self, order_id: UUID) -> Optional[Order]:
        record = self.session.get(OrderRecord, order_id)
        return Order.from_record(record) if record else None

    def save(self, order: Order) -> None:
        record = order.to_record()
        self.session.merge(record)
        self.session.commit()
```

## Data Modeling

- **dataclasses**: Domain models and internal logic (lightweight, standard library)
- **Pydantic**: API boundaries (validation, JSON schema, OpenAPI)
- **Entity transformations**: `from_request()`, `to_response()`, `from_record()`, `to_record()`

See [references/data-modeling.md](references/data-modeling.md) for validation patterns, Pydantic features, and transformation examples.

## Best Practices

1. **Pure functions first** - Write business logic without IO dependencies
2. **Entity encapsulation** - Keep business rules inside entities
3. **Repository abstraction** - Hide storage details, work with domain entities
4. **Validate at boundaries** - Use Pydantic at API edges, simple validation in entities
5. **Immutable value objects** - Always use `frozen=True`
6. **Single Responsibility** - Each layer has one reason to change
7. **Dependency direction** - Always depend on abstractions, not implementations

## Anti-Patterns

❌ **Anemic Domain Model** - Entities with only getters/setters, all logic in services
❌ **Transaction Script** - All logic in service layer, entities just data
❌ **Leaky Abstraction** - Repository exposing database details
❌ **God Object** - Entity with too many responsibilities
❌ **Mixed Concerns** - Business logic calling IO directly

For detailed examples, patterns, and decision trees, see the reference materials:
- [references/functional-core.md](references/functional-core.md) - Core vs shell separation
- [references/ddd.md](references/ddd.md) - DDD patterns, aggregates, bounded contexts
- [references/data-modeling.md](references/data-modeling.md) - dataclasses, Pydantic, transformations

Overview

This skill codifies a Python application architecture that separates a functional core from an imperative shell, applies Domain-Driven Design, and promotes type-safe data modeling. It provides concrete layer responsibilities, entity/value-object patterns, repository abstractions, and guidance for using dataclasses and Pydantic at boundaries. The goal is maintainable, testable systems where pure business logic is isolated from IO and infrastructure.

How this skill works

The architecture enforces a bottom-up dependency flow: Router/Handler → Service → Repository → Entity → Database. Entities and value objects encapsulate business rules and transformations (from_request, to_record, to_response) while repositories hide storage details behind interfaces. Services orchestrate workflows using pure functions in the functional core; the imperative shell performs IO, validation at boundaries, and coordinates repositories and handlers.

When to use it

  • Designing a new Python application that must remain testable and maintainable over time
  • Separating pure business logic from IO for easier unit testing and reasoning
  • Defining domain models, aggregates, and bounded contexts in complex domains
  • Implementing validation and API contracts using Pydantic at system edges
  • Migrating legacy code to a layered architecture to reduce coupling

Best practices

  • Keep business logic in pure functions and entity methods; avoid IO in core logic
  • Model domain concepts with dataclasses for entities and frozen dataclasses for value objects
  • Expose storage via abstract repository interfaces; implement concrete repos in the shell
  • Validate inputs at boundaries with Pydantic and transform to domain entities before business logic
  • Give each layer a single responsibility and depend on abstractions, not implementations

Example use cases

  • Order management system: Order entity holds business rules, repository persists records, service orchestrates workflows
  • Billing domain: Money value object enforces currency rules and immutability, services compute invoices purely
  • Microservice API: Pydantic models validate requests/responses; handlers delegate to services that use repositories
  • Incremental refactor: Extract pure functions and entities from a transactional script to reduce side effects
  • Multi-bounded-context design: Separate aggregates per context and use repositories to translate between records and entities

FAQ

When should I use Pydantic vs dataclasses?

Use dataclasses for internal domain entities and immutable value objects; use Pydantic at API boundaries for validation, parsing, and OpenAPI generation.

How do I keep entities pure while persisting state?

Keep persistence in repository implementations. Transform entities to record DTOs (to_record) and reconstruct with from_record; avoid IO calls inside entity methods.