When your Laravel app integrates with external systemsâthird-party APIs, CMS platforms via CLI, payment gatewaysâstandard database transactions can’t protect you. If your Laravel transaction succeeds but the external call fails (or vice versa), you’re left with inconsistent state across systems.
The solution: create external records in a draft or pending state, complete all Laravel operations within a transaction, and only promote the external resource to published/active after the transaction commits.
The Pattern
public function handle(): void {
$createdExternalIds = [];
foreach ($this->items as $item) {
try {
// Step 1: Create in external system as DRAFT
$externalId = $this->createExternalResource($item, status: 'draft');
$createdExternalIds[] = $externalId;
// Step 2: Laravel DB transaction
DB::transaction(function () use ($item, $externalId) {
$record = Record::create([
'external_id' => $externalId,
'title' => $item['title'],
'status' => 'pending'
]);
$record->tags()->sync($item['tags']);
// ... other database operations
});
// Step 3: Success! Publish the external resource
$this->publishExternalResource($externalId);
} catch (Throwable $e) {
report($e);
$this->error(sprintf('%s: %s', $item['title'], $e->getMessage()));
continue; // Keep processing other items
}
}
}
Why This Works
If the Laravel transaction fails for any reasonâvalidation, constraint violation, or application errorâthe external record remains in draft state. You can clean it up later, retry the import, or manually investigate. Nothing went live that shouldn’t have.
If the external API call succeeds but Laravel fails, you still have a draft record in the external system. Your database stays clean.
Only when both sides succeed does the external record become visible to users.
When to Use This
- Content management systems: Create posts/pages as drafts, link them in Laravel, then publish.
- Third-party APIs: If the API supports draft/pending states (Stripe payment intents, Shopify draft orders, etc.).
- Batch imports: Processing hundreds or thousands of records where you want partial success rather than all-or-nothing.
Implementation Tips
1. Abstract the external calls into methods
private function createExternalResource(array $data, string $status): string
{
$result = Http::post('https://api.example.com/resources', [
'title' => $data['title'],
'status' => $status,
]);
return $result->json('id');
}
private function publishExternalResource(string $id): void
{
Http::patch("https://api.example.com/resources/{$id}", [
'status' => 'published'
]);
}
2. Use continue on errors to process remaining items
In batch operations, don’t let one failure kill the entire import. Log it, report it, move on.
3. Consider cleanup jobs for orphaned drafts
If your process crashes halfway through, you might have draft records in the external system with no corresponding Laravel record. Schedule a daily cleanup job that checks for drafts older than 24 hours and deletes them.
Real-World Example: CMS Integration
Let’s say you’re building a product catalog in Laravel that syncs to a headless CMS for public display. Each Laravel Product needs a corresponding CMS post.
DB::transaction(function () use ($productData) {
// Create CMS post as draft
$cmsPostId = $this->cms->createPost([
'title' => $productData['name'],
'status' => 'draft',
'content' => $productData['description']
]);
// Create Laravel product
$product = Product::create([
'name' => $productData['name'],
'sku' => $productData['sku'],
'cms_post_id' => $cmsPostId,
]);
$product->categories()->sync($productData['category_ids']);
// Publish the CMS post
$this->cms->publishPost($cmsPostId);
});
If anything inside the transaction failsâduplicate SKU, missing category, whateverâthe CMS post stays as a draft. Your public site never shows broken data.
When You Can’t Use Drafts
Not every external system supports draft states. In those cases:
- Do the Laravel work first, then make the external call after the transaction commits.
- Use queued jobs for the external callâif it fails, retry logic kicks in.
- Store the external state in a pending column in Laravel, update it to synced after success.
But if the external system does support drafts or pending states, use them. It’s the cleanest way to maintain consistency across both systems.
Leave a Reply