← Back to articles
Performance & Databases

Event Sourcing in Practice: Not Just Data Versioning, But a Powerful Analytics Tool

A practical guide to the Event-Sourced Entity pattern in Laravel: full audit trails, point-in-time reconstruction, and why event logs are a goldmine for business analytics.

✦
Featured article ↗

Event Sourcing in Practice: Not Just Data Versioning, But a Powerful Analytics Tool

Today, I want to discuss an architectural pattern common in complex business applications but often underappreciated: the "Event-Sourced Entity" or, simply put, a robust data versioning system.

What is it?

In the classic CRUD model, we update a record directly in a table like products: price = 1000. Change history is silently lost.

The event-driven model works differently:

  1. The main entity stores only the current state (e.g., latest price, stock level).
  2. Every change (an event) is recorded in a separate log table as an independent, immutable entry.
  3. The current state is the result of sequentially applying all historical events.

A Simple Laravel Example

Let's consider a ProductPricing entity.

1. Migrations:

// Main table (current state) Schema::create('product_pricings', function (Blueprint $table) { $table->id(); $table->foreignId('product_id')->constrained(); $table->decimal('price', 10, 2); $table->integer('stock'); $table->timestamps(); }); // Events table (history) Schema::create('pricing_events', function (Blueprint $table) { $table->id(); $table->foreignId('product_pricing_id')->constrained()->onDelete('cascade'); $table->string('type'); // 'price_changed', 'stock_incremented' $table->json('payload'); // { "old_price": 100, "new_price": 120, "reason": "supplier_cost" } $table->timestamp('performed_at'); $table->foreignId('user_id')->nullable(); // Who initiated it });

2. Model and Event:

// Event Data Transfer Object class PriceChangedEvent { public function __construct( public float $oldPrice, public float $newPrice, public string $changedBy, public ?string $reason = null ) {} } // ProductPricing Model class ProductPricing extends Model { public function events() { return $this->hasMany(PricingEvent::class); } public function changePrice(float $newPrice, User $user, string $reason): void { // 1. Record the event $this->events()->create([ 'type' => 'price_changed', 'payload' => new PriceChangedEvent( oldPrice: $this->price, newPrice: $newPrice, changedBy: $user->email, reason: $reason ), 'performed_at' => now(), 'user_id' => $user->id, ]); // 2. Update the current state $this->update(['price' => $newPrice]); } }

Advantages of This Approach:

✅ Complete Audit Trail & Traceability. We know not only what changed, but when, by whom, and why. Invaluable for compliance and resolving disputes.

✅ Point-in-Time State Reconstruction. We can "replay" the model to any past moment by recalculating events up to that point.

✅ Decoupled Business Logic. Events are pure facts. They can be processed asynchronously, sent to message queues (RabbitMQ, Kafka).

✅ Foundation for CQRS. The event log is a perfect single source of truth for building various read models (e.g., specialized for reporting).

Drawbacks and Complexities:

❌ Increased Complexity. Architecture becomes more complex than "simple CRUD." Consistency between the main table and events must be managed.

❌ Performance Overhead. Instead of one update, we perform at least two writes. Overkill for high-throughput systems where history is unnecessary.

❌ Querying History. Analyzing data requires working with large event streams, which can be non-trivial.

Where is it Particularly Useful? The Key Use Case: Analytics.

This approach is a goldmine for data analysts and business intelligence. Why?

  1. Trend Analysis & Dynamics: Easily build charts of product price changes over time, analyze seasonality, and correlate with marketing campaigns.
  2. Metric Calculation: Average price over a period, inventory turnover rate, frequency of adjustments.
  3. Root Cause Analysis: Grouping events by reason or user_id reveals what most often drives changes: currency fluctuations, competitor actions, seasonal demand.
  4. "What-If" Modeling: Based on historical events, you can build simulations. "What happens to profit if we increase the price by X% during period Y?"

Example SQL Query for Analytics:

-- Get the price history for a specific product over the last month SELECT DATE(performed_at) as change_date, JSON_UNQUOTE(JSON_EXTRACT(payload, '$.new_price')) as price, JSON_UNQUOTE(JSON_EXTRACT(payload, '$.reason')) as change_reason FROM pricing_events WHERE product_pricing_id = 123 AND type = 'price_changed' AND performed_at >= NOW() - INTERVAL 30 DAY ORDER BY performed_at;

Conclusion

The "Event-Sourced Entity" pattern is more than just "saving the old value." It's a paradigm shift from thinking in "states" to thinking in "processes" and "history."

Use it when:

  • Legislative audit trails are required (pharma, finance, B2G).
  • The business process is a sequence of meaningful events (order: created → paid → fulfilled → shipped).
  • You critically need deep analytics on changing key metrics (prices, inventory, rates, ratings).

It's an investment in architecture that pays off with transparency, flexibility, and data depth for decision-making.

Technologies & topics

Article tags

No articles match these filters.

Have a project or an idea to discuss?

Let's talk ↗