Organizing Your API Codebase is Infrastructure
We are going to adopt "Screaming Architecture" (Robert C. Martin) for our Acme Corp APIs. When you look at the file structure, it should scream "Manufacturing & Logistics," not "Django Project" or "Python Script."
An exhaustive breakdown for the entire framework, applying the Single Responsibility Principle (SRP) to the file system level.
The Core Philosophy: Vertical Slicing + Horizontal Layering
Every Domain (Bounded Context) will follow this exact recursive structure.
domain/: The "What". Pure Python. No libraries.application/: The "How". Orchestration. Clean separation of Commands (Writes) and Queries (Reads).infrastructure/: The "Where". DB adapters, 3rd party APIs.interface/: The "Who". Strawberry GraphQL adapters.
1. The Directory Structure (Exhaustive)
We will use the Warehouse domain as the reference implementation. The same structure applies to Procurement, Logistics, and QualityControl.
src/
├── shared/ # KERNEL: Shared across all domains
│ ├── domain/
│ │ ├── value_objects.py # Weight, Dimensions, UnitOfMeasure
│ │ └── events.py # DomainEvent base class
│ └── infrastructure/
│ └── db.py # SQLAlchemy Base
│
├── domains/
│ ├── warehouse/ # CONTEXT: Inventory Management
│ │ ├── __init__.py # Module export
│ │ │
│ │ ├── domain/ # LAYER: Enterprise Business Rules
│ │ │ ├── aggregates/ # Grouping entities that change together
│ │ │ │ ├── part.py # Class Part(AggregateRoot)
│ │ │ │ └── bin_location.py # Class BinLocation(Entity)
│ │ │ ├── value_objects/ # Domain-specific immutable values
│ │ │ │ ├── sku.py # StockKeepingUnit (Regex Validated)
│ │ │ │ └── safety_stock.py # Min/Max levels
│ │ │ ├── events/ # Events emitted by this domain
│ │ │ │ ├── registered.py # PartRegistered
│ │ │ │ └── stock_low.py # StockLowWarning
│ │ │ └── repositories.py # Interfaces (Abstract Base Classes) only!
│ │ │
│ │ ├── application/ # LAYER: Application Business Rules
│ │ │ ├── commands/ # WRITE SIDE (CQRS) - One file per Use Case
│ │ │ │ ├── register_part.py
│ │ │ │ ├── adjust_stock.py
│ │ │ │ └── decommission_part.py
│ │ │ ├── queries/ # READ SIDE (CQRS)
│ │ │ │ ├── get_part_spec.py
│ │ │ │ └── check_availability.py
│ │ │ └── dtos/ # Data Transfer Objects (Pure Data Classes)
│ │ │ ├── part_dto.py
│ │ │ └── stock_report.py
│ │ │
│ │ ├── infrastructure/ # LAYER: Frameworks & Drivers
│ │ │ ├── persistence/ # Database implementations
│ │ │ │ ├── orm_models.py # SQLAlchemy Tables (NOT Domain Entities)
│ │ │ │ ├── mappers.py # ORM <-> Domain Entity Converter
│ │ │ │ └── postgres_repo.py# Implementation of domain/repositories.py
│ │ │ └── services/ # External Services
│ │ │ └── erp_connector.py# Legacy ERP Sync
│ │ │
│ │ └── interface/ # LAYER: Interface Adapters (Strawberry)
│ │ ├── types/ # GraphQL Output Types
│ │ │ ├── part.py
│ │ │ ├── specs.py
│ │ │ └── location.py
│ │ ├── inputs/ # GraphQL Input Types
│ │ │ ├── registration.py
│ │ │ └── adjustments.py
│ │ ├── mutations/ # GraphQL Mutation Resolvers
│ │ │ ├── inventory.py # Connects GQL -> App Command
│ │ │ └── logistics.py
│ │ └── queries/ # GraphQL Query Resolvers
│ │ └── catalog.py # Connects GQL -> App Query
│ │
│ └── procurement/ # CONTEXT: Purchasing (Same structure as above)
│
└── main.py
2. Implementation Rules (Pedantic)
A. The Domain Layer (Strict Purity)
File: src/domains/warehouse/domain/aggregates/part.py
Rule: This file cannot import strawberry, sqlalchemy, or pydantic. It imports only from shared or sibling domain files.
from typing import Optional
from src.shared.domain.value_objects import Weight
from ..value_objects.sku import SKU
from ..events.registered import PartRegistered
class Part:
def __init__(self, id: str, sku: SKU, name: str, weight: Weight):
self.id = id
self.sku = sku
self.name = name
self.weight = weight
self.is_active = False
self.bin_location_id: Optional[str] = None
def activate(self):
"""Domain Rule: Cannot activate a part with zero weight."""
if self.weight.value <= 0:
raise ValueError("Cannot activate part with zero or negative weight")
self.is_active = True
# Return event to be dispatched by Application Layer
return PartRegistered(part_id=self.id, sku=self.sku.value)
def assign_location(self, bin_id: str):
self.bin_location_id = bin_id
B. The Application Layer (Granular CQRS)
We do not use a generic PartService class. That is an anti-pattern that leads to "God Classes." We create Command Handlers.
File: src/domains/warehouse/application/commands/register_part.py
from dataclasses import dataclass
from src.shared.domain.value_objects import Weight
from ...domain.repositories import PartRepository
# Note: We import the Domain Entity, but we don't expose it. We return a DTO.
from ...domain.aggregates.part import Part
from ...domain.value_objects.sku import SKU
@dataclass
class RegisterPartCommand:
sku_code: str
name: str
weight_val: float
unit: str
class RegisterPartHandler:
def __init__(self, repo: PartRepository):
self.repo = repo
def handle(self, cmd: RegisterPartCommand) -> str:
# 1. Validation & Value Object creation
sku = SKU(cmd.sku_code) # Validates regex format (e.g., "ACM-123")
weight = Weight(cmd.weight_val, cmd.unit)
# 2. Domain Logic
part = Part(
id=self.repo.next_identity(),
sku=sku,
name=cmd.name,
weight=weight
)
# 3. Persistence
self.repo.save(part)
return part.id
C. The Infrastructure Layer (The Mapper Pattern)
Rule: Never let your ORM models leak into the Domain or Application layers.
File: src/domains/warehouse/infrastructure/persistence/mappers.py
from .orm_models import PartModel # The SQLAlchemy Class
from ...domain.aggregates.part import Part # The Domain Class
from ...domain.value_objects.sku import SKU
from src.shared.domain.value_objects import Weight
class PartMapper:
@staticmethod
def to_domain(model: PartModel) -> Part:
part = Part(
id=model.id,
sku=SKU(model.sku),
name=model.name,
weight=Weight(model.weight_val, model.weight_unit)
)
if model.is_active:
part.is_active = True
return part
@staticmethod
def to_persistence(entity: Part) -> PartModel:
return PartModel(
id=entity.id,
sku=entity.sku.value,
name=entity.name,
weight_val=entity.weight.value,
weight_unit=entity.weight.unit,
is_active=entity.is_active
)
D. The Interface Layer (Strawberry Adapters)
We treat GraphQL as just another delivery mechanism. It translates GQL types to Commands.
File: src/domains/warehouse/interface/mutations/inventory.py
import strawberry
from ...application.commands.register_part import (
RegisterPartCommand,
RegisterPartHandler
)
from ..inputs.registration import PartRegistrationInput
from ..types.part import PartType
@strawberry.type
class InventoryMutations:
@strawberry.mutation
def register_part(
self,
info,
input: PartRegistrationInput
) -> PartType:
# 1. Extract Handler from DI Container (in info.context)
handler: RegisterPartHandler = info.context["di"].resolve(RegisterPartHandler)
# 2. Map Input -> Command
command = RegisterPartCommand(
sku_code=input.sku,
name=input.name,
weight_val=input.weight,
unit=input.unit
)
# 3. Execute
new_id = handler.handle(command)
# 4. Return dummy type or fetch fresh (CQRS separation)
return PartType(id=new_id, name=input.name, sku=input.sku)
3. The "Why" behind this Granularity
-
Conflict Minimization:
- Developer A works on
application/commands/register_part.py. - Developer B works on
application/commands/adjust_stock.py. - Result: Zero merge conflicts. A
services.pyfile would have caused conflicts.
- Developer A works on
-
Testability:
- You can unit test
RegisterPartHandlerby mocking thePartRepository. You don't need to spin up the API or the DB.
- You can unit test
-
Replaceability:
- If you want to switch from SQLAlchemy to MongoDB, you only delete
infrastructure/persistence/postgres_repo.pyand writemongo_repo.py. The Domain, Application, and GraphQL layers do not change.
- If you want to switch from SQLAlchemy to MongoDB, you only delete
4. Testing
Because your Domain and Application layers are pure Python (no DB dependencies), you can test 80% of your business logic in milliseconds without spinning up a database.
Here is how we integrate a World-Class Testing Strategy into your specific "Screaming Architecture."
The Testing Philosophy: The Testing Trophy
We don't just dump files in a tests folder. The tests mirror the layers.
- Domain Tests (Unit): Test business rules. No Mocks. No DB. Blazing fast.
- Application Tests (Unit with Mocks): Test orchestration. Mock the Repositories. Verify the flow.
- Infrastructure Tests (Integration): Test the SQL. Real DB. Slower, but necessary.
- Interface Tests (E2E): Test the Contract. GraphQL inputs/outputs.
A. The Directory Structure (Mirrored)
We mirror src exactly. This makes it obvious where a test belongs.
tests/
├── conftest.py # Global Fixtures (DB, Client)
├── shared/
│ └── factories.py # Domain Object Factories
├── domains/
│ ├── warehouse/
│ │ ├── domain/ # PURE UNIT TESTS
│ │ │ └── test_part.py # Tests aggregate logic
│ │ ├── application/ # MOCKED UNIT TESTS
│ │ │ └── test_register_handler.py
│ │ ├── infrastructure/ # INTEGRATION TESTS (DB)
│ │ │ └── test_postgres_repo.py
│ │ └── interface/ # API TESTS
│ │ └── test_inventory_mutations.py
B. Implementation: Layer by Layer
B.1. Domain Layer Tests (Pure Logic)
File: tests/domains/warehouse/domain/test_part.py
Why this is great: We test complex rules without needing a database or a web server.
import pytest
from src.shared.domain.value_objects import Weight
from src.domains.warehouse.domain.aggregates.part import Part
from src.domains.warehouse.domain.value_objects.sku import SKU
def test_cannot_activate_part_with_zero_weight():
# Arrange
part = Part(
id="p1",
sku=SKU("ACM-999"),
name="Anti-Gravity Bolt",
weight=Weight(0, "kg")
)
# Act & Assert
with pytest.raises(ValueError, match="Cannot activate part with zero"):
part.activate()
def test_activation_emits_event():
part = Part("p1", SKU("ACM-999"), "Bolt", Weight(1, "kg"))
event = part.activate()
assert part.is_active is True
assert event.sku == "ACM-999"
B.2. Application Layer Tests (Orchestration)
File: tests/domains/warehouse/application/test_register_handler.py
Why this is great: We verify the flow (validation -> creation -> persistence) using Mocks. We don't care if the DB works here; we assume the Repository interface holds true.
from unittest.mock import Mock
from src.domains.warehouse.application.commands.register_part import (
RegisterPartCommand,
RegisterPartHandler
)
from src.domains.warehouse.domain.repositories import PartRepository
def test_handler_saves_part_correctly():
# 1. Mock the Repository (Infrastructure)
mock_repo = Mock(spec=PartRepository)
mock_repo.next_identity.return_value = "part_777"
# 2. Setup Command
cmd = RegisterPartCommand(
sku_code="ACM-555",
name="Steel Sheet",
weight_val=50.0,
unit="kg"
)
# 3. Execute Handler
handler = RegisterPartHandler(repo=mock_repo)
result_id = handler.handle(cmd)
# 4. Assert Interactions
assert result_id == "part_777"
# Verification: Did we actually call save?
mock_repo.save.assert_called_once()
# Deep verification: Did we save the right data?
saved_part = mock_repo.save.call_args[0][0]
assert saved_part.sku.value == "ACM-555"
B.3. Infrastructure Layer Tests (Persistence)
File: tests/domains/warehouse/infrastructure/test_postgres_repo.py
Why this is great: This is the only place we test SQL/ORM mapping. If we switch to MongoDB, we only rewrite this test file.
from src.domains.warehouse.infrastructure.persistence.postgres_repo import PostgresPartRepository
from tests.shared.factories import PartFactory
from src.domains.warehouse.domain.value_objects.sku import SKU
def test_repo_persists_part(db_session):
# db_session comes from conftest.py (Real DB transaction)
repo = PostgresPartRepository(db_session)
# Create Domain Entity via Factory
part = PartFactory(sku=SKU("ACM-TEST"))
# Save
repo.save(part)
db_session.flush() # Force SQL generation
# Retrieve
fetched = repo.get_by_sku("ACM-TEST")
# Assert
assert fetched is not None
assert fetched.id == part.id
B.4. Interface Layer Tests (Contract)
File: tests/domains/warehouse/interface/test_inventory_mutations.py
Why this is great: Ensures Strawberry is configured correctly and inputs map to commands.
def test_register_part_mutation(client):
mutation = """
mutation {
registerPart(input: {
sku: "ACM-001",
name: "Turbine Blade",
weight: 12.5,
unit: "kg"
}) {
id
sku
}
}
"""
# We mock the DI container in the client fixture to return a real or mocked handler
response = client.post("/graphql", json={"query": mutation})
assert response.status_code == 200
data = response.json()
assert data["data"]["registerPart"]["sku"] == "ACM-001"
C. The Factories (Crucial Helper)
To make Domain testing easy, we need a way to generate complex Aggregate Roots quickly.
File: tests/shared/factories.py
import factory
from src.domains.warehouse.domain.aggregates.part import Part
from src.domains.warehouse.domain.value_objects.sku import SKU
from src.shared.domain.value_objects import Weight
class PartFactory(factory.Factory):
class Meta:
model = Part
id = factory.Sequence(lambda n: f"part_{n}")
sku = factory.Sequence(lambda n: SKU(f"ACM-{n}"))
name = "Standard Widget"
weight = Weight(10.0, "kg")
# We handle the __init__ arguments matching the Domain Entity
5. JSON Generation for Scaffolding
This JSON reflects the exhaustive structure required.
{
"project_root": "acem_corp_erp",
"structure": {
"src": {
"shared": {
"domain": ["value_objects.py", "events.py", "exceptions.py"],
"infrastructure": ["db.py", "bus.py"]
},
"domains": {
"warehouse": {
"domain": {
"aggregates": ["part.py", "bin_location.py"],
"value_objects": ["sku.py", "safety_stock.py"],
"events": ["registered.py"],
"repositories.py": null
},
"application": {
"commands": ["register_part.py", "adjust_stock.py"],
"queries": ["get_spec.py"],
"dtos": ["part_dto.py"]
},
"infrastructure": {
"persistence": ["orm_models.py", "mappers.py", "postgres_repo.py"]
},
"interface": {
"types": ["part.py"],
"inputs": ["registration.py"],
"mutations": ["inventory.py"],
"queries": ["catalog.py"],
"__init__.py": null
},
"__init__.py": null
},
"procurement": {
"domain": { "aggregates": ["purchase_order.py"] },
"__init__.py": null
}
},
"main.py": null
}
},
"tests": {
"conftest.py": "DB Fixtures & Client",
"shared": {
"factories.py": "Domain Object Factories"
},
"domains": {
"warehouse": {
"domain": ["test_part.py"],
"application": ["test_register_handler.py"],
"infrastructure": ["test_postgres_repo.py"],
"interface": ["test_inventory_mutations.py"]
}
}
}
}