transactional-patterns
Skill35 starsChanged 5 months agoMITView on GitHub
Use when managing Doctrine transaction boundaries, flush strategy, optimistic/pessimistic locking, or ensuring consistency across multiple writes in Symfony. Use when the task mentions transaction, flush, locking, concurrency, race condition, or unit of work.
1 other project uses a version of this.
Adopted by
Most recently taken up by
- rrezartprebreza/spring-boot-skillstheir own version, first seen Aug 7, 2026
Other versions1 project uses a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 1
- rrezartprebreza/spring-boot-skills★ 344same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository16 other files it gives its agents.
- api-platform-resourcesSkill
- bundle-organizationSkill
- console-commandsSkill
- doctrine-migrationsSkill
- doctrine-ormSkill
- doctrine-query-optimizationSkill
- domain-driven-designSkill
- dto-and-validationSkill
- hexagonal-architectureSkill
- jwt-authenticationSkill
- layered-architectureSkill
- messenger-asyncSkill
- problem-details-rfc9457Skill
- rest-controller-conventionsSkill
- security-authenticationSkill
- testing-pyramidSkill
The file
We split this skill into the rules it gives an agent
3 rules. About 0 are common, 0 are distinctive, and 3 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
3
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 40
When several operations must succeed or fail together (and a single flush() isn't enough — e.g.
- Very specific: line 94
Avoid triggering flush() inside Doctrine lifecycle callbacks or event listeners mid-request — it reorders the UoW and causes subtle bugs.
- Very specific: line 98
When a write must also notify another system, don't open a transaction across HTTP/queue boundaries.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Transactional Patterns (Doctrine + Symfony)
One flush per request/use case
Doctrine's Unit of Work batches changes; flush() is the commit boundary. Call it once, at the end of the use case, in the service layer — not after every persist(), and never in controllers, repositories, or entities.
// ✅ GOOD — service owns the boundary, single flush
public function placeOrder(CreateOrderRequest $request): Order
{
$order = Order::create($request->customerEmail);
foreach ($request->items as $line) {
$order->addItem(Uuid::fromString($line->productId), $line->quantity);
}
$this->inventory->reserve($order); // also just persists, doesn't flush
$this->em->persist($order);
$this->em->flush(); // one commit for the whole use case
return $order;
}
// ❌ BAD — flush in a loop: N transactions, partial writes on failure
foreach ($lines as $line) {
$em->persist($line);
$em->flush(); // commits each line separately
}Explicit transactions for multi-step consistency
When several operations must succeed or fail together (and a single flush() isn't enough — e.g. raw DBAL + ORM, or you need to react before commit), wrap them:
$this->em->wrapInTransaction(function () use ($order, $payment): void {
$this->em->persist($order);
$this->em->persist($payment);
// any exception in here rolls everything back
});wrapInTransaction() (Doctrine ORM 3) begins, flushes, commits, and rolls back on exception. Prefer it over manual beginTransaction()/commit()/rollback() so you can't forget the rollback path.
Optimistic locking (the default for web apps)
Two users edit the same record; the second overwrites the first ("lost update"). Guard with a #[ORM\Version] column.
#[ORM\Version]
#[ORM\Column(type: Types::INTEGER)]
private int $version = 1;On flush, if the version in the DB changed since you loaded the entity, Doctrine throws OptimisticLockException. Map it to 409 Conflict.
try {
$this->em->flush();
} catch (OptimisticLockException) {
throw new ConcurrentModificationException($order->getId()); // -> 409
}This needs no DB locks and scales — preferred for typical request/response flows.
Pessimistic locking (only for true contention)
For short critical sections with real contention (inventory decrement, seat booking), lock the row at read time inside a transaction:
$this->em->wrapInTransaction(function () use ($id): void {
$product = $this->em->find(
Product::class,
$id,
LockMode::PESSIMISTIC_WRITE, // SELECT ... FOR UPDATE
);
$product->decrementStock(1); // no one else can read-for-update until commit
});Hold the lock for the shortest possible time; long-held row locks cause contention and deadlocks. Don't use pessimistic locking as a default — it serializes access.
flush() only what you mean to
- A
flush()persists every managed entity with pending changes, not just the one you pass. Don't rely onflush($entity)to scope it (removed in ORM 3). - Avoid triggering
flush()inside Doctrine lifecycle callbacks or event listeners mid-request — it reorders the UoW and causes subtle bugs. Defer side effects topostFlush/after commit (see domain events indomain-driven-design).
Cross-service consistency: outbox, not distributed transactions
When a write must also notify another system, don't open a transaction across HTTP/queue boundaries. Persist an outbox row in the same transaction, then dispatch asynchronously (see messenger-async). This keeps the DB write atomic and the side effect eventually consistent.
Gotchas
- Agent calls
flush()inside a loop — flush once after allpersist()calls. - Agent puts
flush()/ transaction logic in a controller, repository, or entity — own it in the service. - Agent uses manual
beginTransaction()/commit()/rollback()and forgets the rollback path — usewrapInTransaction(). - Agent ignores concurrent edits — add
#[ORM\Version]and mapOptimisticLockExceptionto409. - Agent reaches for pessimistic
FOR UPDATElocks by default — use optimistic locking unless there's real contention. - Agent relies on
flush($entity)to scope the commit — ORM 3 flushes the whole UoW. - Agent opens a transaction spanning an external HTTP/queue call — use the outbox pattern + async dispatch.
- Agent calls
flush()inside a Doctrine lifecycle listener — defer topostFlush/after commit.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

