← Back to articles
Performance & Databases

Freezing Time: What Are Database Snapshots and How They Simplify a Developer's Life

A practical guide to the Snapshot pattern: immutable copies of aggregate state for audit, Event Sourcing performance, and point-in-time reporting, with a hands-on Laravel example.

Freezing Time: What Are Database Snapshots and How They Simplify a Developer's Life
Featured article ↗

Freezing Time: What Are Database Snapshots and How They Simplify a Developer's Life

If you work with databases, especially in the context of complex Domain-Driven Design (DDD), event-driven programming, or analytics, you've likely faced this problem: how to efficiently work with an object whose state changes dozens of times?

Imagine an Order entity. It goes through the lifecycle: created → paid → shipped → delivered → returned. Each status is a change in the database. The change history can be huge. But what if you need to:

  • Restore the order's state on a specific date?
  • Analyze how its total price changed over time?
  • Optimize queries for an aggregate with a complex, deep structure?

This is where the Snapshot pattern comes to the rescue.

What is a Snapshot?

A Snapshot is an immutable copy of an object's state (an aggregate in DDD terms) at a specific point in time. It is not a change log (delta), but a complete "photograph" of all the object's important data at that moment.

Real-life analogy: It's like saving a full copy of a document Contract_v1.2_final_updated_2024-12-01.docx before making edits. You can always go back to that exact version.

Why are they needed? 3 Key Reasons

  1. Historical Record & Audit. The most obvious use case. You can know precisely what a user's balance, order composition, or article text was exactly one month ago. This is critical for financial systems and systems with legal audit requirements (e.g., GDPR, SOX).
  2. Performance Boost (Event Sourcing). In Event Sourcing architecture, an object's state is recreated by sequentially applying all events from its history (OrderCreated, ItemAdded, OrderPaid). If there are thousands of events, replaying them all on every query is expensive. The solution: periodically save a snapshot of the aggregate (version #100, #200, etc.). To restore the latest state, you load the latest snapshot (#300) and apply only the events after it (#301 to #350). This dramatically reduces the load.
  3. Point-in-Time Capture for Reporting. To build an analytical report (e.g., "Revenue for last quarter"), you need data fixed at the period's end. Using snapshots guarantees that subsequent changes won't distort the historical picture.

How to Work with Them? Hands-on Laravel (with Eloquent)

Let's implement a simple snapshot system for an Invoice entity.

1. Database Structure

// Main invoices table Schema::create('invoices', function (Blueprint $table) { $table->id(); $table->string('number'); $table->decimal('amount', 10, 2); $table->string('status'); // draft, paid, cancelled $table->json('items'); // Array of items as JSON $table->timestamps(); }); // Table for snapshots Schema::create('invoice_snapshots', function (Blueprint $table) { $table->id(); $table->foreignId('invoice_id')->constrained()->cascadeOnDelete(); $table->json('state'); // The ENTIRE invoice state at snapshot time $table->string('reason')->nullable(); // Creation reason (e.g., "before_payment", "monthly_archive") $table->timestamp('snapshot_at'); // The moment the state was captured $table->timestamps(); });

2. Model and Snapshot Creation

// app/Models/Invoice.php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; class Invoice extends Model { protected $casts = [ 'items' => 'array', ]; // Relationship with snapshots public function snapshots(): HasMany { return $this->hasMany(InvoiceSnapshot::class); } // Method to create a snapshot public function createSnapshot(string $reason = null): InvoiceSnapshot { // Collect ALL crucial data $state = [ 'number' => $this->number, 'amount' => $this->amount, 'status' => $this->status, 'items' => $this->items, // You can include related data if it's critical for context // 'client_name' => $this->client->name, 'created_at' => $this->created_at->toIso8601String(), 'updated_at' => $this->updated_at->toIso8601String(), ]; return $this->snapshots()->create([ 'state' => $state, 'reason' => $reason, 'snapshot_at' => now(), // or carbon->clone() ]); } } // app/Models/InvoiceSnapshot.php namespace App\Models; use Illuminate\Database\Eloquent\Model; class InvoiceSnapshot extends Model { protected $casts = [ 'state' => 'array', 'snapshot_at' => 'datetime', ]; public function invoice() { return $this->belongsTo(Invoice::class); } // Helper accessors to easily get data from the snapshot state public function getInvoiceNumberAttribute() { return $this->state['number']; } public function getInvoiceAmountAttribute() { return $this->state['amount']; } }

3. Usage in Code

// Create an invoice $invoice = Invoice::create([ 'number' => 'INV-2024-001', 'amount' => 1500.00, 'status' => 'draft', 'items' => [['name' => 'SEO Service', 'price' => 1500]], ]); // Take a snapshot before a critical change (e.g., payment) $invoice->createSnapshot('before_payment'); // Apply the change $invoice->update(['status' => 'paid', 'amount' => 1470.00]); // After applying a discount // Now we have history: $lastSnapshot = $invoice->snapshots()->latest->first(); echo $lastSnapshot->invoice_number; // 'INV-2024-001' echo $lastSnapshot->invoice_amount; // 1500.00 (old amount) echo $invoice->amount; // 1470.00 (current amount) // Find snapshots as of a specific date $snapshotsOnDate = InvoiceSnapshot::where('snapshot_at', '<=', '2024-12-01 23:59:59')->get();

Important Considerations and Takeaways

  • Data Depth: Decide what goes into state. Just the aggregate's fields or also related data (e.g., client name). The latter simplifies reading but duplicates data.
  • Frequency: Snapshots can be created on events (before payment), on a schedule (daily), or by version (every N changes).
  • Storage: JSON in the database is convenient for complex objects. For analytics, snapshots can be sent to a separate storage (e.g., a Data Warehouse).
  • Tooling: For Laravel, packages like spatie/laravel-activitylog facilitate logging, and orangehill/iseed helps with seeding, but for snapshots, a custom implementation often fits specific needs best.

In summary: Snapshots are a powerful pattern for working with time in your application. They transform your database from a static store of "current state" into a time machine for your data, providing auditability, performance, and reliability.

Technologies & topics

Article tags

No articles match these filters.

Have a project or an idea to discuss?

Let's talk ↗