Use Repositories in Business Logic Entry Points
Goal
Every business-logic entry point that persists, retrieves, or deletes domain entities must do so exclusively through repositories.
A repository is an abstraction that encapsulates the persistence and retrieval of domain entities. It exposes operations expressed in domain terms — such as create, update, find, delete — and hides the underlying persistence technology.
Business-logic entry points must not call the project's ORM, database library, framework persistence API, query builder, or any other persistence technology directly. All creation, reading, updating, and deletion of domain entities must go through a repository.
This rule applies regardless of the persistence technology in use: relational databases, document stores, key-value stores, object stores, or any other storage mechanism.
What Counts as In Scope
Apply this skill to code that does one or more of these things:
- defines a business-logic entry point that creates, reads, updates, or deletes domain entities
- calls ORM methods, query builders, database clients, or framework persistence APIs directly from a business-logic entry point
- imports or references persistence-technology modules directly from a business-logic entry point
- bypasses a repository to perform domain-entity persistence operations inline
The Rule
Access domain-entity persistence only through repositories.
- Use a repository to create, read, update, and delete domain entities.
- Do not call ORM methods, query builders, raw SQL, database clients, or framework persistence APIs from business-logic entry points.
- Do not import or reference persistence-technology modules from business-logic entry points.
Repositories expose domain-term operations.
- Repository method names must express domain intent and follow the canonical operation set:
findBy<Field>(s) (single-row, returns an absence-permitting type), findManyBy<Field>(s) (multi-row, collection), search (dynamic listings), countBy<Field>(s) (numeric aggregation), existsBy<Field>(s) / existManyBy<Field>(s) (boolean checks that delegate to the corresponding countBy*), create, update, deleteById. See business-logic-entry-point-repository-operations.
- Repository method signatures must use domain-entity types and domain value types, not persistence-layer types.
One repository per domain entity or aggregate.
- Each domain entity or aggregate root has its own repository.
- Do not create generic or catch-all repositories that operate on multiple unrelated entities.
The entry point depends on the repository, not on the persistence technology.
- The entry point receives or references the repository, not the ORM, database client, or connection.
- Transaction management at the entry-point level is a separate concern and is allowed alongside repository usage when another skill requires it.
Detection Workflow
Find business-logic entry points that persist, retrieve, or delete domain entities.
- Identify command handlers, query handlers, use cases, or application services that create, read, update, or delete domain entities.
Check for direct persistence-technology usage.
- Look for imports of ORM modules, database client libraries, query builders, or framework persistence APIs in entry-point files.
- Look for direct calls to ORM methods such as
create, findOne, query, exec, save, update, destroy, insert, select, or their equivalents in the project's persistence technology.
- Look for raw SQL strings, query builder chains, or database client method calls in entry-point code.
Check for repository usage.
- Verify that a repository is used for every domain-entity persistence operation.
- Verify that the repository exposes domain-term operations, not persistence-technology passthrough methods.
Check repository method signatures.
- Verify that repository methods accept and return domain-entity types and domain value types.
- Flag methods that expose persistence-layer types such as ORM entities, database records, row objects, or document models in their signatures.
Writing or Changing Entry Points
Identify the domain entities involved.
- Determine which domain entities the entry point needs to create, read, update, or delete.
Use or create a repository for each domain entity.
- If a repository already exists, use it.
- If no repository exists, create one that exposes the needed operations in domain terms.
- Follow the project's existing convention for repository placement, naming, and structure.
Replace direct persistence-technology calls with repository calls.
- Replace ORM calls, query builder chains, raw queries, and database client calls with repository method calls.
- Remove persistence-technology imports from the entry-point file.
Pass transaction context through the repository when needed.
- When the
business-logic-entry-point-execution-context skill is active, the repository retrieves the transaction from the execution context internally. Do not pass the transaction as a parameter.
- When the execution context skill is not active, pass the transaction context to the repository explicitly so it participates in the same transaction.
- The repository does not own or manage the transaction lifecycle.
Examples
Use this (with execution context):
function createOrderCommandHandler(
command: CreateOrderCommand,
): ResultAsync<CreateOrderCommandHandlerSuccess, CreateOrderCommandHandlerError> {
return runWithinContext(() =>
runWithinTransaction({ isolationLevel: "REPEATABLE READ" }, () =>
ensureRequesterIsAuthenticated()
.andThen((requesterId) =>
orderRepository.create(Order.create(command.customerId, command.items))
),
),
)
}
Not this:
function createOrderCommandHandler(
command: CreateOrderCommand,
): ResultAsync<CreateOrderCommandHandlerSuccess, CreateOrderCommandHandlerError> {
return runWithinContext(() =>
runWithinTransaction({ isolationLevel: "REPEATABLE READ" }, () =>
ensureRequesterIsAuthenticated()
.andThen((requesterId) =>
prisma.order.create({ data: { /* ... */ } })
),
),
)
}
Use this (explicit passing, for languages without execution context):
function createOrderCommandHandler(
command: CreateOrderCommand,
): ResultAsync<CreateOrderCommandHandlerSuccess, CreateOrderCommandHandlerError> {
return runWithinTransaction({ isolationLevel: "REPEATABLE READ" }, (transaction) =>
ensureRequesterIsAuthenticated(command.requesterId)
.andThen((requesterId) =>
orderRepository.create(transaction, Order.create(command.customerId, command.items))
)
)
}
Not this:
function createOrderCommandHandler(
command: CreateOrderCommand,
): ResultAsync<CreateOrderCommandHandlerSuccess, CreateOrderCommandHandlerError> {
return runWithinTransaction({ isolationLevel: "REPEATABLE READ" }, (transaction) =>
ensureRequesterIsAuthenticated(command.requesterId)
.andThen((requesterId) =>
prisma.order.create({ data: { /* ... */ } })
)
)
}
Use this:
def find_customer_by_id_query_handler(
query: FindCustomerByIdQuery,
) -> FindCustomerByIdQueryHandlerSuccess:
with run_within_transaction(isolation_level="READ UNCOMMITTED") as tx:
# find_by_id returns Customer | None — absence is a valid result,
# not an error. The entry point decides what to do with it.
customer = customer_repository.find_by_id(tx, query.customer_id)
return FindCustomerByIdQueryHandlerSuccess(customer=customer)
Not this:
def find_customer_by_id_query_handler(
query: FindCustomerByIdQuery,
) -> FindCustomerByIdQueryHandlerSuccess:
with run_within_transaction(isolation_level="READ UNCOMMITTED") as tx:
customer = session.query(CustomerModel).filter_by(id=query.customer_id).first()
return FindCustomerByIdQueryHandlerSuccess(customer=customer)
Use this:
fun cancelSubscriptionCommandHandler(
command: CancelSubscriptionCommand,
): CancelSubscriptionCommandHandlerSuccess {
return runWithinTransaction(isolationLevel = IsolationLevel.REPEATABLE_READ) { tx ->
// findById returns Subscription? — the entry point converts
// absence into the appropriate domain error for this use case.
val subscription = subscriptionRepository.findById(tx, command.subscriptionId)
?: throw SubscriptionNotFoundError(command.subscriptionId)
val cancelled = subscription.cancel()
subscriptionRepository.update(tx, cancelled)
}
}
Not this:
fun cancelSubscriptionCommandHandler(
command: CancelSubscriptionCommand,
): CancelSubscriptionCommandHandlerSuccess {
return runWithinTransaction(isolationLevel = IsolationLevel.REPEATABLE_READ) { tx ->
val entity = entityManager.find(SubscriptionEntity::class.java, command.subscriptionId)
entity.status = "cancelled"
entityManager.merge(entity)
}
}
Review Questions
When reading or reviewing code, ask:
- Does this entry point create, read, update, or delete domain entities?
- Does it use a repository for every domain-entity persistence operation?
- Does it import or call any ORM, database client, query builder, or framework persistence API directly?
- Do the repository methods use domain-term names and domain-entity types in their signatures?
- Is the persistence technology hidden behind the repository abstraction?
If any entry point accesses domain-entity persistence without going through a repository, apply this skill.
Report the Outcome
When finishing the task:
- state which entry points were identified or changed
- state which repositories were used or created
- state which direct persistence-technology calls were replaced with repository calls
- state which persistence-technology imports were removed from entry-point files
1---2name: business-logic-entry-point-use-repositories3description: Require business-logic entry points to access domain-entity persistence exclusively through repositories. Use when an agent needs to create, modify, review, or interpret business-logic entry points that create, read, or modify domain entities. Entry points must not call the project's ORM, database library, framework persistence API, or any other persistence technology directly. All persistence operations on domain entities must go through a repository.4---56# Use Repositories in Business Logic Entry Points78## Goal910Every business-logic entry point that persists, retrieves, or deletes domain entities must do so exclusively through repositories.1112A repository is an abstraction that encapsulates the persistence and retrieval of domain entities. It exposes operations expressed in domain terms — such as create, update, find, delete — and hides the underlying persistence technology.1314Business-logic entry points must not call the project's ORM, database library, framework persistence API, query builder, or any other persistence technology directly. All creation, reading, updating, and deletion of domain entities must go through a repository.1516This rule applies regardless of the persistence technology in use: relational databases, document stores, key-value stores, object stores, or any other storage mechanism.1718## What Counts as In Scope1920Apply this skill to code that does one or more of these things:2122- defines a business-logic entry point that creates, reads, updates, or deletes domain entities23- calls ORM methods, query builders, database clients, or framework persistence APIs directly from a business-logic entry point24- imports or references persistence-technology modules directly from a business-logic entry point25- bypasses a repository to perform domain-entity persistence operations inline2627## The Rule28291. Access domain-entity persistence only through repositories.30 - Use a repository to create, read, update, and delete domain entities.31 - Do not call ORM methods, query builders, raw SQL, database clients, or framework persistence APIs from business-logic entry points.32 - Do not import or reference persistence-technology modules from business-logic entry points.33342. Repositories expose domain-term operations.35 - Repository method names must express domain intent and follow the canonical operation set: `findBy<Field>(s)` (single-row, returns an absence-permitting type), `findManyBy<Field>(s)` (multi-row, collection), `search` (dynamic listings), `countBy<Field>(s)` (numeric aggregation), `existsBy<Field>(s)` / `existManyBy<Field>(s)` (boolean checks that delegate to the corresponding `countBy*`), `create`, `update`, `deleteById`. See `business-logic-entry-point-repository-operations`.36 - Repository method signatures must use domain-entity types and domain value types, not persistence-layer types.37383. One repository per domain entity or aggregate.39 - Each domain entity or aggregate root has its own repository.40 - Do not create generic or catch-all repositories that operate on multiple unrelated entities.41424. The entry point depends on the repository, not on the persistence technology.43 - The entry point receives or references the repository, not the ORM, database client, or connection.44 - Transaction management at the entry-point level is a separate concern and is allowed alongside repository usage when another skill requires it.4546## Detection Workflow47481. Find business-logic entry points that persist, retrieve, or delete domain entities.49 - Identify command handlers, query handlers, use cases, or application services that create, read, update, or delete domain entities.50512. Check for direct persistence-technology usage.52 - Look for imports of ORM modules, database client libraries, query builders, or framework persistence APIs in entry-point files.53 - Look for direct calls to ORM methods such as `create`, `findOne`, `query`, `exec`, `save`, `update`, `destroy`, `insert`, `select`, or their equivalents in the project's persistence technology.54 - Look for raw SQL strings, query builder chains, or database client method calls in entry-point code.55563. Check for repository usage.57 - Verify that a repository is used for every domain-entity persistence operation.58 - Verify that the repository exposes domain-term operations, not persistence-technology passthrough methods.59604. Check repository method signatures.61 - Verify that repository methods accept and return domain-entity types and domain value types.62 - Flag methods that expose persistence-layer types such as ORM entities, database records, row objects, or document models in their signatures.6364## Writing or Changing Entry Points65661. Identify the domain entities involved.67 - Determine which domain entities the entry point needs to create, read, update, or delete.68692. Use or create a repository for each domain entity.70 - If a repository already exists, use it.71 - If no repository exists, create one that exposes the needed operations in domain terms.72 - Follow the project's existing convention for repository placement, naming, and structure.73743. Replace direct persistence-technology calls with repository calls.75 - Replace ORM calls, query builder chains, raw queries, and database client calls with repository method calls.76 - Remove persistence-technology imports from the entry-point file.77784. Pass transaction context through the repository when needed.79 - When the `business-logic-entry-point-execution-context` skill is active, the repository retrieves the transaction from the execution context internally. Do not pass the transaction as a parameter.80 - When the execution context skill is not active, pass the transaction context to the repository explicitly so it participates in the same transaction.81 - The repository does not own or manage the transaction lifecycle.8283## Examples8485Use this (with execution context):8687```ts88function createOrderCommandHandler(89 command: CreateOrderCommand,90): ResultAsync<CreateOrderCommandHandlerSuccess, CreateOrderCommandHandlerError> {91 return runWithinContext(() =>92 runWithinTransaction({ isolationLevel: "REPEATABLE READ" }, () =>93 ensureRequesterIsAuthenticated()94 .andThen((requesterId) =>95 orderRepository.create(Order.create(command.customerId, command.items))96 ),97 ),98 )99}100```101102Not this:103104```ts105function createOrderCommandHandler(106 command: CreateOrderCommand,107): ResultAsync<CreateOrderCommandHandlerSuccess, CreateOrderCommandHandlerError> {108 return runWithinContext(() =>109 runWithinTransaction({ isolationLevel: "REPEATABLE READ" }, () =>110 ensureRequesterIsAuthenticated()111 .andThen((requesterId) =>112 prisma.order.create({ data: { /* ... */ } })113 ),114 ),115 )116}117```118119Use this (explicit passing, for languages without execution context):120121```ts122function createOrderCommandHandler(123 command: CreateOrderCommand,124): ResultAsync<CreateOrderCommandHandlerSuccess, CreateOrderCommandHandlerError> {125 return runWithinTransaction({ isolationLevel: "REPEATABLE READ" }, (transaction) =>126 ensureRequesterIsAuthenticated(command.requesterId)127 .andThen((requesterId) =>128 orderRepository.create(transaction, Order.create(command.customerId, command.items))129 )130 )131}132```133134Not this:135136```ts137function createOrderCommandHandler(138 command: CreateOrderCommand,139): ResultAsync<CreateOrderCommandHandlerSuccess, CreateOrderCommandHandlerError> {140 return runWithinTransaction({ isolationLevel: "REPEATABLE READ" }, (transaction) =>141 ensureRequesterIsAuthenticated(command.requesterId)142 .andThen((requesterId) =>143 prisma.order.create({ data: { /* ... */ } })144 )145 )146}147```148149Use this:150151```py152def find_customer_by_id_query_handler(153 query: FindCustomerByIdQuery,154) -> FindCustomerByIdQueryHandlerSuccess:155 with run_within_transaction(isolation_level="READ UNCOMMITTED") as tx:156 # find_by_id returns Customer | None — absence is a valid result,157 # not an error. The entry point decides what to do with it.158 customer = customer_repository.find_by_id(tx, query.customer_id)159 return FindCustomerByIdQueryHandlerSuccess(customer=customer)160```161162Not this:163164```py165def find_customer_by_id_query_handler(166 query: FindCustomerByIdQuery,167) -> FindCustomerByIdQueryHandlerSuccess:168 with run_within_transaction(isolation_level="READ UNCOMMITTED") as tx:169 customer = session.query(CustomerModel).filter_by(id=query.customer_id).first()170 return FindCustomerByIdQueryHandlerSuccess(customer=customer)171```172173Use this:174175```kt176fun cancelSubscriptionCommandHandler(177 command: CancelSubscriptionCommand,178): CancelSubscriptionCommandHandlerSuccess {179 return runWithinTransaction(isolationLevel = IsolationLevel.REPEATABLE_READ) { tx ->180 // findById returns Subscription? — the entry point converts181 // absence into the appropriate domain error for this use case.182 val subscription = subscriptionRepository.findById(tx, command.subscriptionId)183 ?: throw SubscriptionNotFoundError(command.subscriptionId)184 val cancelled = subscription.cancel()185 subscriptionRepository.update(tx, cancelled)186 }187}188```189190Not this:191192```kt193fun cancelSubscriptionCommandHandler(194 command: CancelSubscriptionCommand,195): CancelSubscriptionCommandHandlerSuccess {196 return runWithinTransaction(isolationLevel = IsolationLevel.REPEATABLE_READ) { tx ->197 val entity = entityManager.find(SubscriptionEntity::class.java, command.subscriptionId)198 entity.status = "cancelled"199 entityManager.merge(entity)200 }201}202```203204## Review Questions205206When reading or reviewing code, ask:207208- Does this entry point create, read, update, or delete domain entities?209- Does it use a repository for every domain-entity persistence operation?210- Does it import or call any ORM, database client, query builder, or framework persistence API directly?211- Do the repository methods use domain-term names and domain-entity types in their signatures?212- Is the persistence technology hidden behind the repository abstraction?213214If any entry point accesses domain-entity persistence without going through a repository, apply this skill.215216## Report the Outcome217218When finishing the task:219220- state which entry points were identified or changed221- state which repositories were used or created222- state which direct persistence-technology calls were replaced with repository calls223- state which persistence-technology imports were removed from entry-point files