Note: This document must be kept in sync with the actual codebase. When making structural changes (adding layers, modules, or changing patterns), update this document accordingly.
src/
├── common/ # Common foundation (no dependencies on other modules)
│ ├── config/
│ │ └── interface.py # ConfigIf: Base config interface
│ ├── di/
│ │ └── builder.py # ModuleBase, BindEntry: DI module base
│ └── interface.py # Interface = ABC alias
│
└── system/ # Main system (depends only on common)
├── domain/ # Domain layer (business logic)
│ ├── config.py # DomainConfigIf, EnvironmentEnum
│ ├── model/ # Domain models
│ │ ├── base.py # DomainModelBase (Pydantic BaseModel + Interface)
│ │ ├── user.py # User
│ │ ├── messenger.py # Messenger
│ │ └── nickname.py # NicknameChangelog
│ ├── value/ # Value objects
│ │ ├── base/
│ │ │ ├── identifier.py # ULIDBase (RootModel[ULID])
│ │ │ └── time.py # CreatedAtBase, UpdatedAtBase (RootModel[datetime])
│ │ ├── user.py # UserRecordID, UserID, UserCreatedAt, etc.
│ │ ├── messenger.py # MessengerRecordID, MessengerName, etc.
│ │ └── nickname.py # NicknameChangelogRecordID, Nickname, etc.
│ └── interface/
│ └── repository/ # Repository interfaces
│ ├── common/
│ │ ├── base.py # RepositoryBase
│ │ ├── response.py # RepositoryResponse[T], RepositoryResultStatusEnum, etc.
│ │ └── option.py # SortOrder
│ ├── user.py # UserRepository
│ ├── messenger.py # MessengerRepository
│ └── nickname.py # NicknameChangelogRepository
│
├── infrastructure/ # Infrastructure layer (external system integration)
│ ├── repository/
│ │ └── sqlalchemy/ # SQLAlchemy implementation
│ │ ├── config.py # SQLAlchemyConfigIf
│ │ ├── model/ # SQLAlchemy models (ULIDMixin, CreatedAtMixin, etc.)
│ │ ├── crud/ # Repository implementations (SAUserRepository, etc.)
│ │ ├── translator/ # Domain <-> DB conversion (SAUserTranslator, etc.)
│ │ └── type/ # Custom types (ULIDColumn)
│ └── ext/ # External services
│ ├── sentry/ # Sentry error tracking
│ └── logfire/ # Logfire observability
│
├── usecase/ # Usecase layer (application logic)
│ └── base/
│ ├── dto.py # UsecaseRequest, UsecaseResponse (Pydantic BaseModel)
│ └── interface.py # UsecaseIf[RequestT, ResponseT].execute()
│
├── ui/ # UI layer (external interfaces)
│ └── discord/
│ └── config.py # DiscordConfigIf
│
├── di/ # Dependency injection
│ ├── container.py # DIContainer (Injector)
│ └── module/ # DI module definitions
│ ├── domain/config/ # PydanticDomainConfigModule
│ ├── infrastructure/
│ │ ├── repository/sqlalchemy/ # SARepositoryModule
│ │ └── ext/ # Sentry, Logfire modules
│ └── ui/discord/config/ # PydanticDiscordConfigModule
│
├── util/ # Utilities
│ ├── id.py # generate_ulid()
│ └── datetime.py # utcnow()
│
└── config.py # Environment-specific config (BaseConfig, TestConfig, etc.)
ui → usecase → domain ← infrastructure
↓
common
- common: Common foundation. No dependencies on other modules
- domain: Business logic. Depends on common only
- usecase: Application logic. Depends on domain (and common)
- infrastructure: External system implementation. Implements domain interfaces (depends on domain and common)
- ui: User interface. Calls usecase (depends on usecase and common)
-
Repository Pattern
- Interface:
src/system/domain/interface/repository/{entity}.py - Implementation:
src/system/infrastructure/repository/sqlalchemy/crud/{entity}.py - Response:
RepositoryResponse[T]withis_success,status,reason,message
- Interface:
-
Translator Pattern (Domain ⇔ DB)
BaseSADomainTranslator[DomainT, SAModelT]- Static methods:
to_domain(),to_db_record() - Lazy imports inside methods to avoid circular dependencies
-
Value Object Pattern
- Wrap primitives with
RootModel[T] - ULID identifiers extend
ULIDBase - Timestamps extend
CreatedAtBase/UpdatedAtBase
- Wrap primitives with
-
Usecase Pattern
- Request/Response DTOs extend
UsecaseRequest/UsecaseResponse - Interface:
UsecaseIf[RequestT, ResponseT]withasync execute()
- Request/Response DTOs extend
-
DI Pattern
injectorlibrary withModule,@provider,@singleton- Config interfaces bound to Pydantic implementations
- Repositories bound with
singletonscope
| Category | Pattern | Example |
|---|---|---|
| Domain Model | {Entity} |
User, Messenger |
| Value Object | {Entity}{Field} |
UserRecordID, UserCreatedAt |
| Repository Interface | {Entity}Repository |
UserRepository |
| SQLAlchemy Impl | SA{Entity}Repository |
SAUserRepository |
| Translator | SA{Entity}Translator |
SAUserTranslator |
| Config Interface | {Component}ConfigIf |
DiscordConfigIf |
| Usecase Interface | {Action}UsecaseIf |
(to be implemented) |
- All meaningful operations in usecase, repository, and ui layers must be decorated with
@logfire.instrument(span_name="ClassName.method_name()") - Sentry for error tracking
- All implementations must pass the following checks:
ty check,ruff check --fix,ruff format, andpytest.
- Implementation must proceed from the innermost layer.
- After completing each layer, a review must be conducted before proceeding to the next layer.
- Dependencies under
srcmust be strictly controlled.commonmust not depend on any other modules.systemmay depend oncommon.- External libraries may be used only when their necessity and appropriateness are justified.
- Do not export namespaces via
__init__.py. Always import directly from the module where the symbol is defined.
- When implementing code or creating commits, review existing implementations and keep notation, structure, and style consistent.
In particular, code should follow the conventions used by other implementations within the same layer.
- Test files are colocated with source files in
src/.- Each source file
module.pyhas a corresponding test filetest_module.pyin the same directory. - In principle, a one-to-one correspondence between files should be maintained.
- Each source file
- Commit message prefixes must be one of the following:
add:— for adding new features or filesmodify:— for modifying existing functionalityremove:— for removing features or filesfix:— for bug fixes
- Identifiers in commit messages must be enclosed in backticks (`):
- Class names:
ClassName - Functions / methods:
function_name(),ClassName.method_name() - Variables / attributes:
variable_name,ClassName.attribute - File names / paths:
file.py,path/to/file.py - Commands:
command
- Class names:
- The output language must follow the language used in the most recent user input.
- Regardless of the user's input or output language, all file contents (including source code), and any other resources managed within the repository must be written in English.
- Exception: Comments and commit messages may be written in Japanese.
- All identifiers (variable names, function names, class names, module names, etc.) must use only English or ASCII characters.
- Write tests to achieve as close to 100% direct coverage as possible for all files.
- If the cost of writing tests is prohibitively high and the disadvantages/costs outweigh the benefits, it is not necessary to force 100% coverage.
- For code that clearly does not need to be included in coverage (e.g.,
passorraise NotImplementedErrorin abstract classes), use# pragma: no coverto exclude it from coverage. - Only when a file has no implementation at all (e.g.,
config.py), exclude the entire file from coverage in./pyproject.toml. However, if there is even a small amount of implementation, this exception does not apply. - Do not use
patch()with string paths as a general rule, as this significantly reduces maintainability. However, it may be used for external libraries. Even in such cases, strive to avoid writing paths as strings by usingpatch.objector similar approaches. - For all system code, anything that needs to be patched during testing should be injected via DI so that mocks can be passed in tests.
- This project uses Sentry and Logfire, so make sure to use both appropriately.