- Knowledge
- technology
- OOP
- Tips
- Programming
- Tips
- Tutorial
- SEO
- Ranking
- Knowledge
- Special Day
- Seo
- Bug
- Data science
- Seo
- artificial intelligence
- Machine Learning
- Robotics
- happyNewYear2021
- newYearEve
- 2021
- Automation
- Smart Home
- Career
- Best Practices
- Git
- Logging
- Web Fundamentals
- DNS
- HTTPS
- Performance
- AI Tools
- ChatGPT
- Claude
- Gemini
- Laravel
- Eloquent
- MySQL
- HTTPS
- TLS
- Web Security
- Certificates
- Developer Life
- Debugging
- Docker
- DevOps
- Transactions
- Queues
- LLMs
- AI
- AI Coding
- Developer Tools
- React Native
- Expo
- Kate PMS
- Mobile Apps
- Laravel
- Authentication
- Sanctum
- Cookies
- API Design
- Payments
- Idempotency
- DeepSeek
- Open Source AI
- LLMs
- AI News
- Git
- Version Control
- AI Coding
- Prompting
- PHP
- Checklist
- MCP
- AI Agents
- OpenAI
- Architecture
- Microservices
- Modular Monolith
- Estimation
- Developer Life
- Project Planning
- Humour
- OAuth
- OpenID Connect
- Authentication
- Embeddings
- Vector Search
- RAG
- pgvector
- OpenAI
- GPT-4.1
- Codex CLI
- Events
- Testing
- Clean Code
- Maintainability
- Code Review
- Webhooks
- API
- Security
- Claude Code
- Workflow
- AI
- LLM
- Prompt Injection
- Mobile
- React
- Networking
- TCP
- UDP
- HTTP/3
- CLAUDE.md
- AWS
- Cloud Security
- Backups
- PHPUnit
- Software Engineering
- Leadership
- Communication
- RAG
- Embeddings
- AI Engineering
- IT Infrastructure
- Networking
- Access Control
- CI/CD
- GitHub Actions
- Gemini CLI
- Claude Code
- JavaScript
- Async/Await
- Node.js
- Promises
- Security
- Cryptography
- Passwords
- MySQL
- Database
- Vibe Coding
- Software Quality
- DNS
- Code Reading
- Onboarding
- Productivity
- Background Jobs
- Developer Humour
- Estimates
- Dev Life
- JWT
- o3-mini
- DeepSeek R1
- Rate Limiting
- Kate PMS
- E-Signing
- Audit Trail
- REST
- GraphQL
- API Design
- Laravel 12
- Upgrade Guide
- Open Source
- Self-Hosting
- Task Scheduling
- Cron
- Secrets
- CORS
- PHP
- PHP-FPM
- OPcache
- GitHub Copilot
- Software Architecture
- Engineering
- TypeScript
- JavaScript
- Type Safety
- AI Security
- React Native
- Product Design
- AI Agents
- Kiro
- Queues
- Redis
- RabbitMQ
- AWS SQS
- Nginx
- Apache
- GPT-5
- gpt-oss
- Clean Code
- Architecture
- Naming
- Documentation
- Career
- ADR
- Teamwork
- Supply Chain
- Kate HRM
- HR Software
- Permissions
- System Design
- Pagination
- SSH
- Linux
- Big O
- Databases
- Laravel Boost
- MCP
- Developer Skills
- Validation
- Databases
- Indexes
- Code Quality
- Deployment
- Developer Humour
- Feature Flags
- Code Review
- Pull Requests
- Docker
- Cursor
- Authorization
- RBAC
- Gemini
- Long Context
- PHP 8.4
- Caching
- Dependency Injection
- Web Performance
- Browser
- CSS
- Database
- Migrations
- ChatGPT
- AI for Developers
- Monitoring
- On-Call
- REST
- Backend
- SQL
- NoSQL
- Database Design
- Coding Agents
- Claude 4
- API Resources
- REST API
- Load Balancing
- Scaling
- AWS
- AI Tools
- Claude
- Sora 2
- CTE
- 2FA
- TOTP
- Programming Languages
- Prompts
- Developer Workflow
- API Gateway
- APIs
- Passport
- API Auth
- Learning
- Burnout
- Developer Growth
- Web Development
- SEO
- Kate Mall
- ChatGPT Atlas
- Agent Skills
- Middleware
- Laravel 12
- Collections
- Context Window
- Monitoring
- Commit Messages
- Self Review
- Growth
- Regex
- Programming Basics
- Text Processing
- Database Design
- Normalization
- Linux
- Server Security
- Linux Foundation
- Open Standards
- Legacy Code
- Documentation
- AI Workflow
- File Uploads
- Test Data
- Hashing
- Performance
- Caching
- Enums
- Scope Creep
- Estimation
- Codex
- Gemini CLI
- Timezones
- Carbon
- Bugs
- PHP 8.5
- Gemini 3
- GPT-5.1
- Data Integrity
- Event Loop
- Async
- Opus 4.5
- AI Models
- React
- Forms
- Frontend
- Backups
- AI Images
- DALL-E
- Midjourney
- Race Conditions
- Concurrency
- Legacy Code
- Refactoring
- Senior Engineer
- Scope
- LLM
- CDN
- Web
- Sub-Agents
- Soft Deletes
- Audit Log
- Concurrency
- AI Learning
- NestJS
- AI Evals
- Policies
- SPF DKIM DMARC
- Unicode
- UTF-8
- Knowledge Graph
- Value Objects
- Technical Debt
- Feature Flags
- Laravel Pennant
- Deployment
- Copilot
- Composer
- Dependencies
- Artisan
- Automation
- AWS S3
- Object Storage
- Cloud
- Small Language Models
- Ollama
- Production
- Sessions
- HTTP
- Mentoring
- SQL
- Virtual Machines
- Web Development
- HTTP/2
- QUIC
- Web Performance
- AI Integration
- LLM API
- SOLID
- OOP
- Hosting
- Serverless
- Merge Conflicts
- Temperature
- AI Development
- Reverse Proxy
- Nginx
- Infrastructure
- Verification
- Passkeys
- WebAuthn
- Teams
- Communication
- Stakeholders
- Monorepo
- CI/CD
- Versioning
- JSON Schema
- Livewire
- Inertia
- Meetings
- Distributed Systems
- Privacy
- Full-Stack
- T-Shaped Skills
- Money
- Notifications
- Web Security
- HTTP Headers
- CSP
- Function Calling
- Load Testing
- k6
- Data Extraction
- Debugging
- WebSockets
- SSE
- Real-Time
- Laravel Reverb
- Infrastructure as Code
- Terraform
- Side Projects
- Laravel Pint
- OpenAPI
- Swagger
- UX
- Multimodal
- Jest
- Pair Programming
- APIs
- Rate Limiting
- Resilience
- Dev Humour
- Design Tokens
- JWT
- API Keys
- Sessions
- PHPStan
- Rector
- Incidents
- Reporting
- Dashboards
- Zero Trust
- IAM
- Search
- Laravel Scout
- Junior Developers
- Mentoring
- Images
- WebP
- AVIF
- Bug Reports
- Let's Encrypt
- Design Docs
- Software Design
- Observers
- Replication
- Accountability
- Data Structures
- Reliability
- LLM Memory
- Error Handling
- Payments
- Payment Gateway
- Webhooks
- PCI DSS
- Observability
- OpenTelemetry
- Personal Brand
- Writing
- Conventions
- Dates
- Scheduling
- Disaster Recovery
- Compression
- Brotli
- Deadlines
- Developer Habits
- State Machines
- Tech Roles
- UUID
- ULID
- Horizon
- Planning
- Engineering Culture
- Ownership
- Soft Skills
- Socialite
- Cost Control
- Collations
- Unicode
- Octane
- PostgreSQL
Database Transactions in Laravel: When You Need Them and When You Don't
About Post
A tenant pays rent. Your code creates a payment record, reduces the invoice balance, and writes a ledger entry. Three queries. The first two succeed, then the third fails because of a typo in a column name that only that code path hits.
Now the money is recorded, the balance is reduced, and the ledger has no idea any of it happened. Someone in finance will find that mismatch weeks later, and nobody will enjoy the afternoon that follows.
That's the problem database transactions solve. But transactions are also one of those tools people either never use or wrap around everything. Let's look at when you need them, when you don't, and the one trap that catches almost everyone: side effects.
What a transaction gives you
A transaction groups several queries into one unit: either all of them are saved, or none of them are. If anything fails in the middle, the database rolls back to how it was before you started.
In Laravel, the cleanest way to use one is the closure form:
use Illuminate\Support\Facades\DB;
DB::transaction(function () use ($invoice, $amount) {
$invoice->payments()->create(['amount' => $amount]);
$invoice->decrement('balance', $amount);
LedgerEntry::create([
'invoice_id' => $invoice->id,
'amount' => $amount,
'type' => 'rent_payment',
]);
});
If the closure finishes, Laravel commits. If it throws any exception, Laravel rolls back and re-throws the exception so you still see the error. Whatever the closure returns, DB::transaction() returns too, which is handy for passing back the created model.
You can also call DB::beginTransaction(), DB::commit() and DB::rollBack() yourself, but the closure is harder to get wrong. With the manual version, it's very easy to forget the rollback in one catch branch.
When you need one
My simple test: would it be a problem if only some of these writes happened? If yes, use a transaction.
- Money moving: payments, refunds, wallet balances, invoice totals.
- Parent and children together: an order and its line items, a contract and its payment schedule.
- Read-then-write decisions: "if the balance is enough, deduct it" (more on this below).
- Moving something between states: marking a unit as leased while creating the lease.
When you don't
- A single query. One
INSERTorUPDATEis already atomic on its own. - Read-only code. There's nothing to roll back.
- Writes that are fine independently. Logging a page view and updating a "last seen" timestamp don't need to succeed together.
- The whole request "just in case". Long transactions hold locks longer, which makes other requests wait and makes deadlocks more likely. Keep transactions as short as the work that truly belongs together.
Transactions don't stop race conditions by themselves
This surprises people. Two requests can both read the same balance, both decide there's enough, and both deduct it, each inside its own perfectly valid transaction.
When a decision depends on what you just read, lock the row while you decide:
$payment = DB::transaction(function () use ($invoiceId, $amount) {
$invoice = Invoice::whereKey($invoiceId)->lockForUpdate()->firstOrFail();
if ($amount > $invoice->balance) {
throw new OverpaymentException();
}
$invoice->decrement('balance', $amount);
return $invoice->payments()->create(['amount' => $amount]);
}, 3);
lockForUpdate() makes the second request wait until the first transaction finishes, so it reads the updated balance. The lock only works inside a transaction, which is one more reason they go together.
That second argument, 3, is the number of attempts. If the database reports a deadlock, Laravel retries the whole closure up to that many times before giving up. Locking code makes deadlocks more likely under load, so a small retry count is a cheap safety net. Just make sure everything inside the closure is safe to run again, which leads to the real trap.
The trap: side effects inside the transaction
The database can roll back. Your email server can't. Neither can your queue, a payment gateway, or an SMS provider.
Look at what goes wrong here:
DB::transaction(function () use ($contract) {
$contract->update(['status' => 'signed']);
Mail::to($contract->tenant)->send(new ContractSigned($contract));
GenerateContractPdf::dispatch($contract);
$contract->schedule()->createMany($this->installments($contract));
});
Two different bugs are hiding in it:
- If
createManyfails, the transaction rolls back, but the tenant has already received an email saying their contract is signed. You can't unsend it. - Even when everything succeeds, the queued job might be picked up by a worker before the transaction commits. The worker queries the database, doesn't see the signed contract yet, and fails or processes stale data. This one is intermittent, which makes it miserable to debug.
The fix: run side effects after commit
Laravel has first-class support for this:
DB::transaction(function () use ($contract) {
$contract->update(['status' => 'signed']);
$contract->schedule()->createMany($this->installments($contract));
GenerateContractPdf::dispatch($contract)->afterCommit();
DB::afterCommit(fn () => Mail::to($contract->tenant)
->queue(new ContractSigned($contract)));
});
->afterCommit()on a dispatch holds the job until the transaction commits. If it rolls back, the job is never dispatched.DB::afterCommit()does the same for any callback. Outside a transaction, it simply runs immediately.- You can make this the default for a queue connection by setting
'after_commit' => trueinconfig/queue.php. Queued event listeners can implementShouldQueueAfterCommitfor the same behaviour.
And for calls to external APIs, like charging a card? Don't put them inside the transaction at all. A charge can't be rolled back by your database, and a slow gateway would hold your row locks for the whole round trip. Record the intent, call the gateway outside, then record the result in a second short transaction.
The rule: inside a transaction, only touch the database. Everything that leaves your database (emails, jobs, HTTP calls, files) happens after commit.
Two small details worth knowing
- Nested transactions work in Laravel: an inner
DB::transaction()uses a savepoint, so if you catch the inner exception, only the inner work is rolled back and the outer transaction can carry on. - Engine matters. In MySQL, transactions need InnoDB. Old MyISAM tables silently ignore them, which is a lovely surprise in a legacy database.
The short checklist
- Several writes that must succeed together? Transaction.
- Decision based on a value you just read? Transaction plus
lockForUpdate(). - Emails, jobs, events, API calls? After commit.
- Keep it short, and retry on deadlocks.
The Laravel database transactions docs are short and worth a read once.
Have you ever been bitten by a job running before its transaction committed? How long did it take you to work out what was going on?

Be first to comment it...