A repository is a responsibility specialized for I/O of an aggregate.
Design Principles
Naming Conventions
The repository name must be 集約名 + Repository .
# NG: テーブル名ベース
OrdersTableRepository
UserAccountsRepository (テーブル名 user_accounts に由来)
# NG: DTOベース
OrderDtoRepository
UserResponseRepository
OK: Based on aggregate name
OrderRepository UserRepository
**検出基準**: リポジトリ名に `Table`, `Dto`, `Entity`, `Record`, `Row` 等のインフラ用語が含まれている。
### CQS(Command Query Separation)
リポジトリのメソッドはCQSに従う。各メソッドはコマンド(状態変更、戻り値なし)またはクエリ(状態変更なし、値を返す)のいずれかである。
**Query(問い合わせ)**: 集約を返す。副作用なし。
**Command(命令)**: 集約を受け取り、voidを返す。状態を変更する。
### 単件・複数件I/O
**単件I/O**:
```kotlin
// Query: 単件取得
fun findById(id: OrderId): Order?
// Command: 単件保存
fun store(order: Order)
Multiple-item I/O:
// Query: 複数件取得
fun findByIds(ids: List<OrderId>): List<Order>
// Command: 複数件保存
fun storeMulti(orders: List<Order>): Int
Synchronous/Asynchronous Patterns
Repositories can be designed as either synchronous or asynchronous. For errors, choose either the exception approach or the Result/Either approach.
For detailed design guidelines on the error approach, refer to the error-handling skill.
Synchronous (Exception Approach):
// Query: 例外でエラーを通知
fun findById(id: OrderId): Order?
// Command
fun store(order: Order)
Synchronous (Result/Either Approach):
// Query: Result型でエラーを返す
fun findById(id: OrderId): Result<Order?, RepositoryError>
// Command
fun store(order: Order): Result<Unit, RepositoryError>
Asynchronous (Future):
// Query: Futureのエラー機構を使用
def findById(id: OrderId): Future[Option[Order]]
// Command
def store(order: Order): Future[Unit]
Asynchronous (async/await + Result):
// Query
async fn find_by_id(&self, id: &OrderId) -> Result<Option<Order>, RepositoryError>;
// Command
async fn store(&self, order: &Order) -> Result<(), RepositoryError>;
Anti-patterns
The return value of findById is not an aggregate
// NG: DTOを返す
fun findById(id: OrderId): OrderDto
// NG: テーブル行を返す
fun findById(id: OrderId): OrderRecord
// NG: エンティティの一部を返す
fun findById(id: OrderId): OrderSummary
// OK: 集約を返す
fun findById(id: OrderId): Order?
Method names containing domain logic
A repository is responsible only for I/O of an aggregate (save, retrieve, delete). Domain-specific operations must not be included in method names.
// NG: ドメインロジックがリポジトリに漏れている
fun leave(userId: UserId) // 「退会」はドメインの振る舞い
fun activate(orderId: OrderId) // 「有効化」はドメインの振る舞い
fun cancel(orderId: OrderId) // 「キャンセル」はドメインの振る舞い
fun approve(requestId: RequestId) // 「承認」はドメインの振る舞い
fun rename(userId: UserId) // 「名前の変更」はドメインの振る舞い
// NG: DB操作を想起するメソッド名(リポジトリはDB以外の実装もありえるため)
fun insert(order: Order) // INSERT文を連想
fun update(order: Order) // UPDATE文を連想
fun select(id: OrderId): Order? // SELECT文を連想
fun upsert(order: Order) // UPSERT文を連想
// OK: コレクションとしてのI/O操作
fun store(user: User)
fun put(order: Order)
fun add(order: Order)
fun delete(order: Order)
fun findById(id: OrderId): Order?
fun storeMulti(orders: List<Order>): Int
fun putAll(orders: List<Order>)
fun addAll(orders: List<Order>)
fun findByIds(ids: List<OrderId>): List<Order>
Allowed method names:
- Single-item
storefindByIddeleteputremoveadd
- Plural
storeMultifindByIdsdeleteMultiputMultiaddMulti
and similar I/O operations.
Forbidden method names:
- Domain terms:
leave,activate,cancel,approve, etc. - DB operation terms:
insert,update,select,upsert, etc.
Correct design: Domain logic is executed by aggregate methods, and the repository merely saves the result.
// 集約でドメインロジックを実行
val user = userRepository.findById(userId)
val leftUser = user.leave() // 集約のメソッド
// リポジトリは保存するだけ
userRepository.store(leftUser)
Calling another repository from a repository
A repository must not call another repository within its implementation. Coordination between aggregates is the responsibility of the use case layer (application service).
Reason:
- Violation of aggregate boundaries: One repository per aggregate. A dependency on another repository is a sign that aggregate boundaries are not designed correctly
- Mixing of responsibilities: A repository's responsibility is "persistence and restoration of an aggregate," and retrieving other aggregates is not included in that responsibility
- Hidden coupling: Dependencies between aggregates become buried in the infrastructure layer, making them difficult to discover and change
- Testing difficulty: Testing a repository requires mocks of another repository
// NG: リポジトリが別のリポジトリに依存
class OrderRepositoryImpl(
private val productRepository: ProductRepository
) : OrderRepository {
override fun store(order: Order) {
val product = productRepository.findById(order.productId)
// ...
}
}
// OK: ユースケース層で調整
class PlaceOrderUseCase(
private val orderRepository: OrderRepository,
private val productRepository: ProductRepository,
) {
fun execute(command: PlaceOrderCommand) {
val product = productRepository.findById(command.productId)
val order = Order.create(product.id, command.quantity)
orderRepository.store(order)
}
}
Detection criteria: Another repository is referenced in the repository's constructor or methods.
Violations of store's arguments/return values
// NG: DTOを受け取る
fun store(dto: OrderDto)
// NG: 保存後のDBレコードを返す(CQS違反)
fun store(order: Order): OrderRecord
// NG: IDを返す(CQS違反)
fun store(order: Order): OrderId
// OK: 集約を受け取り、voidを返す
fun store(order: Order)
Check procedure
- Identify the repository's interface/trait
- Check whether the name is
集約名 + Repositorycheck whether it is - Check whether each method follows CQS
- Query: returns an aggregate, no side effects
- Command: receives an aggregate, returns void
- check whether the input/output types are aggregates
- if there are violations, present concrete correction proposals
Related skills (recommended to read together)
When using this skill, also refer to the following skills:
repository-placement: Placement location of repository interfaces (use case layer vs. domain layer)aggregate-design: Design rules for aggregates persisted by repositorieserror-handling: Error handling approach in synchronous/asynchronous repository patterns