Product Import Rollback on Failure

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.
Development and maintenance of all types of websites:
Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Showing 1 of 1 servicesAll 2065 services
Product Import Rollback on Failure
Medium
~3-5 business days
FAQ
Our competencies:
Development stages
Latest works
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    822
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    847
  • image_website-sbh_0.png
    Website development for SBH Partners
    999
  • image_website-_0.png
    Website development for Red Pear
    451

Implementation of Rollback for Failed Product Imports

An import with column mapping errors can rewrite prices with incorrect data for thousands of products. Without a rollback mechanism, the only option is to restore from a database backup, which takes hours and brings down the entire site. Import rollback returns the catalog to the "before" state in minutes.

Rollback Strategies

1. Snapshot Before Import (Reliable, Expensive in Space)

Before import, save a snapshot of affected rows:

CREATE TABLE import_product_snapshots (
    id          bigserial PRIMARY KEY,
    import_id   int REFERENCES import_runs(id) ON DELETE CASCADE,
    product_id  int,
    operation   varchar(10),   -- create | update (delete separately)
    data_before jsonb,         -- state BEFORE import (NULL for create)
    created_at  timestamptz DEFAULT now()
);

2. Soft Delete for New Products (Create)

Products created during import are marked deleted_at on rollback — not physically deleted.

3. Changelog / Event Sourcing (For Complex Catalogs)

Each change is written as an event. Rollback = applying reverse events. More complex to implement, but allows rollback to any point in time.

Saving Snapshot Before Processing

class ImportSnapshotService
{
    public function captureBeforeImport(int $importId, array $skus, int $sourceId): void
    {
        // Get existing product data that we'll change
        $products = Product::whereIn('sku', $skus)
            ->where('source_id', $sourceId)
            ->get(['id', 'sku', 'name', 'price', 'qty', 'description',
                   'category_id', 'deleted_at', 'updated_at']);

        $snapshots = $products->map(fn($p) => [
            'import_id'   => $importId,
            'product_id'  => $p->id,
            'operation'   => 'update',
            'data_before' => json_encode($p->toArray()),
            'created_at'  => now()->toDateTimeString(),
        ])->all();

        // Batch insert
        foreach (array_chunk($snapshots, 1000) as $chunk) {
            ImportProductSnapshot::insert($chunk);
        }
    }

    public function captureNewProduct(int $importId, int $productId): void
    {
        ImportProductSnapshot::create([
            'import_id'   => $importId,
            'product_id'  => $productId,
            'operation'   => 'create',
            'data_before' => null,
        ]);
    }
}

Rollback Mechanism

class ImportRollbackService
{
    public function rollback(ImportRun $import): RollbackResult
    {
        if (!in_array($import->status, ['success', 'partial', 'failed'])) {
            throw new \RuntimeException('Import is not in a rollbackable state');
        }

        if ($import->rolled_back_at) {
            throw new \RuntimeException('Import already rolled back');
        }

        $restored = $deleted = 0;

        DB::transaction(function () use ($import, &$restored, &$deleted) {
            $snapshots = ImportProductSnapshot::where('import_id', $import->id)
                ->orderByDesc('id') // reverse order for dependencies
                ->get();

            foreach ($snapshots as $snapshot) {
                if ($snapshot->operation === 'create') {
                    // Created products — delete (soft)
                    Product::find($snapshot->product_id)?->delete();
                    $deleted++;
                } else {
                    // Updated products — restore previous state
                    $before = json_decode($snapshot->data_before, true);
                    Product::where('id', $snapshot->product_id)->update($before);
                    $restored++;
                }
            }

            $import->update([
                'rolled_back_at'  => now(),
                'rolled_back_by'  => auth()->id(),
                'rollback_result' => compact('restored', 'deleted'),
            ]);
        });

        return new RollbackResult($restored, $deleted);
    }
}

Everything executes in a single transaction — either rollback completes fully, or nothing changes.

Incremental Rollback for Large Imports

Rolling back 100,000 rows in one transaction is risky (long lock). Use batching:

public function rollbackInBatches(ImportRun $import, int $batchSize = 1000): void
{
    $totalSnapshots = ImportProductSnapshot::where('import_id', $import->id)->count();
    $offset         = 0;

    while ($offset < $totalSnapshots) {
        DB::transaction(function () use ($import, $batchSize, $offset) {
            $snapshots = ImportProductSnapshot::where('import_id', $import->id)
                ->orderByDesc('id')
                ->skip($offset)
                ->take($batchSize)
                ->get();

            foreach ($snapshots as $snapshot) {
                $this->applySnapshot($snapshot);
            }
        });

        $offset += $batchSize;
        ImportRun::find($import->id)->increment('rollback_progress', $batchSize);
    }
}

Rollback Applicability Conditions

Not every import can be rolled back:

Condition Rollback Possible?
Snapshot saved completely Yes
Less than 7 days ago Yes (retention policy)
Import already cancelled No
New import on top of this one Partially (only untouched rows)
Physically deleted products (not soft delete) No
public function canRollback(ImportRun $import): bool
{
    return !$import->rolled_back_at
        && $import->created_at->isAfter(now()->subDays(7))
        && ImportProductSnapshot::where('import_id', $import->id)->exists();
}

Cascading Rollback of Related Data

Import affects not just the products table. On rollback, account for:

private function applySnapshot(ImportProductSnapshot $snapshot): void
{
    DB::transaction(function () use ($snapshot) {
        if ($snapshot->operation === 'create') {
            // Delete all related data
            ProductImage::where('product_id', $snapshot->product_id)->delete();
            ProductSpec::where('product_id', $snapshot->product_id)->delete();
            ProductFilterValue::where('product_id', $snapshot->product_id)->delete();
            Product::find($snapshot->product_id)?->forceDelete();
        } else {
            $before = json_decode($snapshot->data_before, true);
            Product::where('id', $snapshot->product_id)->update(
                array_intersect_key($before, array_flip(['name', 'price', 'qty', 'description', 'category_id']))
            );
        }
    });
}

Snapshot Storage and TTL

Snapshots take up space. Retention policy:

// Artifacts stored 7 days after successful import
$schedule->command('import:cleanup-snapshots --days=7')->daily();
ImportProductSnapshot::whereHas('run', fn($q) =>
    $q->where('status', 'success')
      ->where('completed_at', '<', now()->subDays(7))
)->delete();

Notification After Rollback

ImportRolledBackNotification::send($import->triggeredBy, $import);
// Email: "Import #1847 rolled back. Restored: 4,312 products, deleted: 88."

Timeline

  • Snapshot before import (captureBeforeImport), basic rollback in transaction — 2 days
  • Rollback batching, cascade through related tables, applicability check — +1 day
  • Admin UI (rollback button, progress status, snapshot TTL) — +1 day