repository-no-business-logic
Repository No Business Logic
Goal
Repositories must contain no business logic. A repository is a persistence gateway — it persists and retrieves domain entities exactly as instructed by its caller.
A repository's responsibility is limited to:
- translating between domain types and persistence representations
- executing persistence operations — save, find, delete
- mapping query parameters to persistence queries
A repository must not:
- validate business rules or enforce domain invariants
- make domain decisions based on the data it reads or writes
- transform domain state — that is the entity's or domain service's responsibility
- filter or exclude results based on business criteria not expressed by the caller
- trigger side effects that represent business behavior
Business logic belongs in domain entities, aggregate roots, domain services, or business-logic entry points. The repository is infrastructure — it does what it is told.
What Counts as In Scope
Apply this skill to code that does one or more of these things:
- implements a repository method — save, find, delete, or equivalent
- adds conditional logic inside a repository that depends on business rules
- adds validation, invariant checks, or domain decisions inside a repository
- adds data transformation that represents a business operation inside a repository
- filters query results inside a repository based on business criteria not specified by the caller
The Rule
-
A repository persists and retrieves exactly what it is asked to.
- A save operation persists the entity as provided — it does not modify, validate, or reject it based on business rules.
- A find operation returns the data matching the caller's criteria — it does not filter, transform, or enrich the result based on business rules.
- A delete operation removes what the caller specifies — it does not check whether deletion is allowed by business rules.
-
No business rule validation in repositories.
- Do not check domain invariants before saving — that is the entity's responsibility.
- Do not validate business constraints before deleting — that is the entry point's responsibility.
- Do not reject operations based on domain state — that decision belongs to the domain layer.
-
No domain state transformation in repositories.
- Do not modify entity fields, recalculate values, or apply defaults that represent business logic.
- The only transformation a repository performs is the technical mapping between domain types and persistence representations — this is infrastructure, not business logic.
-
No business-driven filtering in repositories.
- Repository query methods accept explicit criteria from the caller — IDs, field values, pagination parameters.
- Do not add implicit filters based on business rules that the caller did not request — for example, silently excluding soft-deleted records or filtering by status unless the caller explicitly asks for it.
-
No domain side effects in repositories.
- Do not emit domain events, send notifications, update audit logs with business meaning, or trigger any behavior that represents a business concern.
- Infrastructure-level concerns such as database-level audit columns managed by the persistence technology are acceptable — they are not business logic.
Detection Workflow
-
Read the repository implementation.
- Examine each method — save, find, delete, and any custom query methods.
-
Look for conditional logic that depends on business rules.
- If-statements, guards, or branches that check domain state and alter behavior are a signal.
- Ask: would this condition exist in a repository for a completely different domain entity? If not, it is likely business logic.
-
Look for data transformation beyond persistence mapping.
- If the repository modifies entity state before saving or after reading — beyond converting between domain and persistence types — it may contain business logic.
-
Look for implicit filtering.
- If a find method excludes results that the caller did not ask to exclude, the filter may represent a business rule.
-
Look for side effects.
- If saving or deleting triggers additional behavior beyond the persistence operation, check whether that behavior is a business concern.
Delegation to Execution Context
When the business-logic-entry-point-execution-context skill is active in the project, the repository implementation retrieves the transaction from the execution context instead of receiving it as a parameter. The repository method signature does not include the transaction. When the transaction from the execution context is undefined or null, the repository must create a new standalone transaction for that operation. All other rules from this skill still apply: the repository must contain no business logic.
Examples
Correct — repository does only persistence (with execution context):
class PrismaOrderRepository implements OrderRepository {
constructor(private readonly prisma: PrismaClient) {}
create(order: Order): ResultAsync<Order, RepositoryError> {
const client = getExecutionContext()?.transaction ?? this.prisma;
// maps Order to persistence representation and saves — nothing else
return ResultAsync.fromPromise(
client.order.create({
data: toPersistence(order),
}),
(error) => new RepositoryError(error),
).map(() => order)
}
findById(orderId: OrderId): ResultAsync<Order | null, RepositoryError> {
const client = getExecutionContext()?.transaction ?? this.prisma;
// queries and maps back to domain type — nothing else
return ResultAsync.fromPromise(
client.order.findUnique({ where: { id: orderId } }),
(error) => new RepositoryError(error),
).map((record) => record ? toDomain(record) : null)
}
}
Not this — business logic inside the repository:
class PrismaOrderRepository implements OrderRepository {
constructor(private readonly prisma: PrismaClient) {}
create(order: Order): ResultAsync<Order, RepositoryError> {
const client = getExecutionContext()?.transaction ?? this.prisma;
// Bad: validating a business rule before saving
if (order.items.length === 0) {
return errAsync(new RepositoryError('Order must have at least one item'))
}
// Bad: transforming domain state
const orderWithTotal = { ...order, total: calculateTotal(order.items) }
return ResultAsync.fromPromise(
client.order.create({
data: toPersistence(orderWithTotal),
}),
(error) => new RepositoryError(error),
).map(() => orderWithTotal)
}
findActiveByCustomerId(customerId: CustomerId): ResultAsync<Order[], RepositoryError> {
const client = getExecutionContext()?.transaction ?? this.prisma;
// Bad: implicit business filter — "active" is a business concept
// the caller should request the specific status filter
return ResultAsync.fromPromise(
client.order.findMany({
where: { customerId, status: { not: 'cancelled' } },
}),
(error) => new RepositoryError(error),
).map((records) => records.map(toDomain))
}
}
Correct — repository does only persistence (explicit passing, for languages without execution context):
class PrismaOrderRepository implements OrderRepository {
save(transaction: Transaction, order: Order): ResultAsync<Order, RepositoryError> {
// maps Order to persistence representation and saves — nothing else
return ResultAsync.fromPromise(
transaction.order.upsert({
where: { id: order.id },
create: toPersistence(order),
update: toPersistence(order),
}),
(error) => new RepositoryError(error),
).map(() => order)
}
findById(transaction: Transaction, orderId: OrderId): ResultAsync<Order | null, RepositoryError> {
// queries and maps back to domain type — nothing else
return ResultAsync.fromPromise(
transaction.order.findUnique({ where: { id: orderId } }),
(error) => new RepositoryError(error),
).map((record) => record ? toDomain(record) : null)
}
}
Not this — business logic inside the repository:
class PrismaOrderRepository implements OrderRepository {
save(transaction: Transaction, order: Order): ResultAsync<Order, RepositoryError> {
// Bad: validating a business rule before saving
if (order.items.length === 0) {
return errAsync(new RepositoryError('Order must have at least one item'))
}
// Bad: transforming domain state
const orderWithTotal = { ...order, total: calculateTotal(order.items) }
return ResultAsync.fromPromise(
transaction.order.upsert({
where: { id: order.id },
create: toPersistence(orderWithTotal),
update: toPersistence(orderWithTotal),
}),
(error) => new RepositoryError(error),
).map(() => orderWithTotal)
}
findActiveByCustomerId(transaction: Transaction, customerId: CustomerId): ResultAsync<Order[], RepositoryError> {
// Bad: implicit business filter — "active" is a business concept
// the caller should request the specific status filter
return ResultAsync.fromPromise(
transaction.order.findMany({
where: { customerId, status: { not: 'cancelled' } },
}),
(error) => new RepositoryError(error),
).map((records) => records.map(toDomain))
}
}
Review Questions
When reading or reviewing code, ask:
- Does this repository method validate any business rule or domain invariant?
- Does this repository method transform domain state beyond persistence mapping?
- Does this repository method filter results based on business criteria not specified by the caller?
- Does this repository method trigger side effects that represent business behavior?
- If this logic were removed from the repository, would the repository still fulfill its persistence responsibility?
If any business logic is found inside a repository, extract it to the appropriate domain layer — entity, domain service, or entry point.
Report the Outcome
When finishing the task:
- state which repository methods were reviewed or changed
- state whether any business logic was found inside the repository
- state where the extracted business logic was moved — entity, domain service, or entry point
- state that the repository now contains only persistence operations and type mapping
More from code-sherpas/agent-skills
neverthrow-return-types
Require `neverthrow`-based return types in TypeScript and JavaScript code whenever the surrounding technology allows it. Use when creating, refactoring, reviewing, or extending standalone functions, exported module functions, class methods, object methods, service methods, repository methods, and similar APIs that should expose explicit success and failure result types in their signatures. Prefer `Result<T, E>` for synchronous code and `ResultAsync<T, E>` for asynchronous code. Only skip a `neverthrow` return type when a framework, library, runtime interface, or externally imposed contract is incompatible and requires a different return shape.
16neverthrow-wrap-exceptions
Capture exceptions and promise failures with `neverthrow` instead of hand-written `try/catch` in TypeScript and JavaScript code. Use when wrapping synchronous functions that may throw, promise-returning functions that may throw before returning, existing `PromiseLike` values that may reject, or third-party APIs such as parsers, database clients, HTTP clients, file-system helpers, serializers, and SDK calls. Prefer `Result.fromThrowable` for synchronous throwers, `ResultAsync.fromThrowable` for promise-returning functions that may throw or reject, and `ResultAsync.fromPromise` when you already have a `PromiseLike` value in hand. Only keep `try/catch` when the language construct, cleanup requirement, or framework boundary truly requires it.
11atomic-design
Create or update web UI components with a strict reuse-first workflow. Use when building, refactoring, restyling, or extending frontend or template components while minimizing raw DOM or HTML by reusing or generalizing existing components first.
10write-persistence-representations
Create or update persistence-layer data representations in any stack, including ORM entities, schema definitions, table mappings, document models, collection definitions, and similar database-facing code. Use when agents needs to add or change persisted fields, identifiers, relationships, indexes, timestamps, auditing fields, or storage mappings in frameworks, libraries, or ORMs such as Prisma, TypeORM, Sequelize, Drizzle, Mongoose, Hibernate/JPA, Doctrine, Ecto, Active Record, or equivalent persistence technologies.
7business-logic
Identify, interpret, review, or write business logic in code. Use when an agent needs to decide whether code expresses business rules, business algorithms, or business workflows, or when it must implement, preserve, or refactor code that creates, stores, or transforms data according to real business policies.
7aggregate-boundaries
Determine and enforce aggregate boundaries when domain entities relate to other domain entities. Use when an agent needs to create, modify, review, or interpret a domain entity that references another domain entity. The agent must determine whether the related entities belong to the same aggregate or to different aggregates, apply the correct reference style — direct reference within the same aggregate, identity reference across aggregates — and document the boundary decision in the project's agent instructions file so future tasks reuse the same decision without asking again.
6