business-logic-entry-point-database-transaction
Database Transaction for Business Logic Entry Points
Goal
Every business-logic entry point that interacts with a database must wrap its entire flow in a single database transaction, when the underlying persistence technology supports transactions.
Use the project's existing library, framework, or ORM to open and manage the transaction. Do not introduce a custom transaction mechanism when the project stack already provides one.
The transaction must encompass the full entry-point flow: business constraints, business rules, business operations, and persistence. The entire flow succeeds or fails atomically.
If the persistence technology does not support transactions (e.g., some NoSQL databases, object stores, or file-based storage), this skill does not apply.
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 reads from or writes to a database
- executes multiple database operations within a single entry point without a wrapping transaction
- partially wraps some operations in a transaction while leaving others outside
- manages transaction boundaries inside inner helpers rather than at the entry-point level
The Rule
-
Wrap the entire entry-point flow in a single database transaction.
- The transaction begins before any business constraint that accesses the database.
- The transaction commits only after the entire flow completes successfully.
- The transaction rolls back if any step fails.
-
Use the project's transaction mechanism.
- Use the library, framework, or ORM that the project already uses for database access.
- Follow the project's idiomatic pattern for transaction management, whether that is a decorator, context manager, callback, wrapper function, or explicit begin/commit/rollback.
-
Place the transaction boundary at the entry point, not inside inner helpers.
- The entry point owns the transaction.
- Inner helpers, business constraints, and persistence functions participate in the transaction but do not open their own.
- Do not nest independent transactions within the same entry-point flow.
-
Include all database-accessing steps in the transaction.
- Business constraints that query the database to verify preconditions must run inside the transaction.
- Persistence operations that store or update data must run inside the transaction.
- Read operations that inform business decisions must run inside the transaction.
Detection Workflow
-
Find business-logic entry points that access a database.
- Identify command handlers, query handlers, use cases, or application services that read from or write to a database.
-
Check for a wrapping transaction.
- Verify that a transaction is opened at the entry-point level.
- Verify that the transaction encompasses the entire flow.
-
Check for partial or misplaced transactions.
- Look for transactions opened inside inner helpers rather than at the entry point.
- Look for database operations that execute outside the transaction boundary.
-
Check the transaction mechanism.
- Verify that the project's existing library, framework, or ORM is used.
- Verify that the pattern is idiomatic for the project.
Writing or Changing Entry Points
-
Open the transaction at the entry point.
- Use the project's idiomatic transaction pattern.
- Ensure the transaction wraps the first database-accessing step through the last.
-
Pass the transaction context to inner helpers.
- If the project's transaction mechanism requires an explicit connection, session, or context object, pass it from the entry point to inner functions.
- If the project uses implicit transaction propagation (e.g., thread-local, async context), verify that inner helpers participate in the same transaction.
-
Commit on success, rollback on failure.
- Let the transaction mechanism handle commit and rollback based on the entry-point outcome.
- Do not manually commit partway through the flow.
-
Do not suppress transaction errors.
- If the commit fails, propagate the error through the entry point's error convention.
Delegation to Execution Context
When the business-logic-entry-point-execution-context skill is active in the project, the database transaction must be stored in the execution context instead of being passed explicitly through the call chain.
runWithExecutionContextaccepts an optional transaction parameter that opens the transaction and stores it in the execution context before running the callback. The entry point does not use a separatewithTransactionwrapper.- Inner functions, business constraints, and repository methods retrieve the transaction from the execution context instead of receiving it as a parameter.
- The entry point still owns the transaction lifecycle through
runWithExecutionContext: it opens, commits, and rolls back the transaction. - When a repository retrieves the transaction from the execution context and it is
undefinedornull, the repository must create a new standalone transaction for that operation. This ensures repository methods work both inside a wrapping transaction and outside one. - All other rules from this skill still apply: the transaction wraps the entire entry-point flow, it is opened at the entry-point level, and all database-accessing steps run inside it.
Examples
TypeScript with execution context:
function createReservationCommandHandler(
command: CreateReservationCommand,
): ResultAsync<CreateReservationCommandHandlerSuccess, CreateReservationCommandHandlerError> {
return runWithExecutionContext(
() =>
ensureRequesterIsAuthenticated()
.andThen((requesterId) =>
ensureAvailableCars(command.carClass)
)
.andThen(() =>
persistReservation(reservation)
),
{ transaction: { isolationLevel: "REPEATABLE READ" } },
)
}
TypeScript with a transaction wrapper (explicit passing, for languages without execution context):
function createReservationCommandHandler(
command: CreateReservationCommand,
): ResultAsync<CreateReservationCommandHandlerSuccess, CreateReservationCommandHandlerError> {
return withTransaction((transaction) =>
ensureRequesterIsAuthenticated(command.requesterId)
.andThen((requesterId) =>
ensureAvailableCars(transaction, command.carClass)
)
.andThen(() =>
persistReservation(transaction, reservation)
)
)
}
Python with a context manager:
def create_reservation_command_handler(
command: CreateReservationCommand,
) -> CreateReservationCommandHandlerSuccess:
with transaction() as tx:
requester_id = ensure_requester_is_authenticated(command.requester_id)
ensure_available_cars(tx, command.car_class)
return persist_reservation(tx, reservation)
Kotlin with a framework transaction:
fun createReservationCommandHandler(
command: CreateReservationCommand,
): CreateReservationCommandHandlerSuccess {
return withTransaction { tx ->
val requesterId = ensureRequesterIsAuthenticated(command.requesterId)
ensureAvailableCars(tx, command.carClass)
persistReservation(tx, reservation)
}
}
Review Questions
When reading or reviewing code, ask:
- Does this entry point access a database?
- Is the entire flow wrapped in a single transaction?
- Is the transaction opened at the entry-point level, not inside inner helpers?
- Do all database-accessing steps, including business constraints, run inside the transaction?
- Is the project's existing transaction mechanism used?
If the answer is yes, apply this skill.
Report the Outcome
When finishing the task:
- state which entry points were identified or changed
- state how the transaction wraps the entire entry-point flow
- state which transaction mechanism from the project stack was used
- state whether any database-accessing steps were moved inside the transaction boundary
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.
10business-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.
7immutable-domain-entities
Require the immutable design pattern for domain entities. Use when an agent needs to create, modify, review, or interpret domain entities and should preserve identity while expressing state changes through new immutable instances. Domain entities must be modeled as immutable classes, not as plain type aliases or interfaces paired with standalone functions.
7update-agent-skills
Update agent skills installed with the `skills` CLI. Use when asked to refresh installed skills, keep a project's skills current, or troubleshoot cases where `npx skills update` reports that everything is up to date. For project-scoped installs, a no-change update must immediately run the bundled reinstall script so tracked skills from `skills-lock.json` are reinstalled without extra investigation.
7