f in x
Event Sourcing and CQRS — Why Your Microservices Need Immutable Events
> cd .. / HUB_EDITORIALE
Sviluppo di siti web

Event Sourcing and CQRS — Why Your Microservices Need Immutable Events

[2026-07-07] Author: Ing. Calogero Bono
Zenithby Meteora Web The operating system for your business. Social, clients, bookings and invoices in one platform. Gyms, barbers, professionals. Discover Zenith Free demo · no card

The problem is as old as relational databases: you update a record, you lose history. An order changes status, a price gets modified, a customer fixes an address — and without granular logging, you have no idea what happened. In microservices, where each service owns its data, chaos is inevitable. Event sourcing and CQRS aren't trends: they're the answer to decades of compromise. We came to this managing the ERP system of a clothing store: every inventory movement was a row, not an UPDATE. The same principle applies to distributed software.

Why choose an event-driven architecture instead of classic CRUD?

CRUD (Create, Read, Update, Delete) works fine as long as one user is modifying a record. In a distributed system, multiple services need to react to the same changes. With CRUD you end up writing webhooks, polling, job queues. With event-driven, each change is an event published to a broker (Kafka, RabbitMQ, NATS). Every interested service consumes it independently. Zero polling, zero coupling.

We, at Meteora Web, built a proprietary platform to manage social presence for multiple clients. When a client changed their plan, a single "PlanModified" event updated billing, editorial calendar, and notifications. With CRUD we would have had to coordinate three UPDATEs across three different services — a nightmare of distributed transactions.

Sponsored Protocol

What do you gain economically?

Less integration code, fewer bugs, less time wasted synchronizing data. Initial development cost is higher, but TCO drops as the system scales. We come from accounting: the same principle as journal entries — every event is a double-entry line, immutable, traceable.

How does event sourcing work and what problems does it solve?

Event sourcing flips the logic: instead of saving the current state of an entity, you save the sequence of events that produced it. The current state is derived by replaying events. It's like an accountant's ledger: every transaction is recorded, and the balance is calculated by reading all entries.

Practical example: shopping cart

// Event stored in the event store
class ItemAdded {
    UUID cartId;
    UUID productId;
    int quantity;
    LocalDateTime timestamp;
}

// Reconstructing state
Cart cart = eventStore
    .getEvents(cartId)
    .reduce(Cart::apply, new Cart());

Immediate benefits:

Sponsored Protocol

  • Complete audit trail: every change tracked, with date and actor.
  • Time travel: you can reconstruct state at any point in time — useful for debugging, reports, rollbacks.
  • Service synchronization: each consumer receives events in the correct order.

A drawback? Event versioning. If you change an event's structure, you must handle multiple versions. We use a simple pattern: each event has a version field and an upcaster that transforms it.

CQRS: why separate reads and writes in distributed systems?

CQRS (Command Query Responsibility Segregation) splits command (write) and query (read) models. Commands mutate state, queries read it. In classic CRUD you use the same model for both — and when queries become complex, the write model gets bloated.

With CQRS:

  • Write model: optimized for validations and domain rules. Small aggregates, immutable events.
  • Read model: denormalized, ready for UIs. Can be an SQL database optimized for SELECT, a search engine, or a Redis cache.

A real example: a booking system for a dog groomer. Writes (book, cancel, reschedule) follow strict rules (no overlaps). Reads (monthly calendar) must be blazing fast. With CQRS we separated the two: event store for commands, a denormalized table for queries. The calendar updates in real time and the user never sees stale data.

Sponsored Protocol

Can you have CQRS without event sourcing?

Yes, but you lose the audit trail. Event sourcing is the cleanest way to feed the read model: projections listen to events and update query databases. In practice, almost a must when eventual consistency and traceability are needed.

How to implement event sourcing and CQRS together in a real project?

There's no magic framework, just architectural choices. We always start with a question: "Does the domain need immutable history?" If yes, we proceed:

1. Choose your event store

You can use specialized databases (EventStoreDB) or implement a layer on top of PostgreSQL (append-only tables). We prefer PostgreSQL for small/medium projects: an events table with columns aggregate_id, version, event_type, data (JSONB).

CREATE TABLE events (
    id BIGSERIAL PRIMARY KEY,
    aggregate_id UUID NOT NULL,
    version INT NOT NULL,
    event_type VARCHAR(255) NOT NULL,
    data JSONB NOT NULL,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    UNIQUE(aggregate_id, version)
);

2. Define commands and handlers

A command is a DTO. The handler loads the aggregate from the event store, applies domain logic, produces new events, and saves them in one transaction.

Sponsored Protocol

class AddItemHandler {
    public function __construct(
        private EventStore $eventStore,
        private EventBus $eventBus
    ) {}

    public function __invoke(AddItem $command): void {
        $cart = Cart::reconstruct(
            $this->eventStore->getEvents($command->cartId)
        );
        $events = $cart->addItem($command->productId, $command->quantity);
        $this->eventStore->append($command->cartId, $events);
        $this->eventBus->publish(...$events);
    }
}

3. Build projections (read model)

A consumer listens to events and updates a denormalized table. For instance, an order fulfilled event updates a "RecentOrders" view.

class OrdersProjection {
    public function onOrderFulfilled(OrderFulfilled $event): void {
        DB::table('order_view')->updateOrInsert(
            ['order_id' => $event->orderId],
            ['status' => 'fulfilled', 'fulfilled_at' => $event->timestamp]
        );
    }
}

4. Handle eventual consistency

There's a delay between command and query (usually milliseconds). If your use case requires immediate consistency (e.g., cash withdrawal), CQRS is not suitable. In all other cases, it works well.

Sponsored Protocol

What to do next

Three concrete actions you can take today:

  1. Analyze your domain: where do you need audit trails? Where are reads very different from writes? Those are ideal candidates for event sourcing and CQRS.
  2. Build a prototype with PostgreSQL as event store: implement just one entity (e.g., order or cart) with commands, event store, and a projection. You'll see the benefits immediately.
  3. Don't fall for the "CQRS everywhere" trap: not every microservice needs this complexity. Isolate the bounded contexts that actually benefit.

We, at Meteora Web, have been using these patterns for years in projects built with Laravel and Livewire. Our pillar on Microservices and Distributed Architecture gives you the full picture. For more on how security ties into distributed events, read about the first AI-run ransomware.

Ing. Calogero Bono

> AUTHOR_EXTRACTED

Ing. Calogero Bono

Ingegnere informatico, fondatore di Meteora Web e Zenith OS. System administrator e progettista di piattaforme, app e CMS proprietari, con esperienza in sviluppo full-stack, marketing digitale ed ecosistema Google.
[ Read Full Dossier ]

> METEORA_WEB // DIGITAL AGENCY

We build the digital presence your business deserves.

Websites, social media, online advertising, e-commerce and high-performance hosting, engineered with method by computer engineers in Sciacca, for all of Italy.

> MW_JOURNAL

> READ_ALL()