Toplamalar, Komutlar ve Alan Olayı Modellemesi
Tutarlılık sınırlarını uygularken komutları doğrulayan ve alan olayları yayan toplamalar tasarlayın.
Toplamalar, Komutlar ve Alan Olayı Modellemesi, CoddyKit'te ücretsiz bir Node.js Backend Development Bootcamp dersidir. Bu, 4 dersinin 2. dersidir. Aşağıdan dersin tamamını ücretsiz okuyabilir, sonra tarayıcıda yerleşik kod editörü ve 7/24 yapay zeka koçu ile uygulamalı olarak pratik yapabilirsin. Bu, Node.js Backend Development Bootcamp öğrenme yolunun bir parçasıdır ve ilerlemeniz web ve CoddyKit uygulaması arasında senkronize olur. Node.js Backend Development Bootcamp kursu toplamda 4 dersten oluşur.
Bu dersin bazı bölümleri henüz çevrilmemiş olup İngilizce olarak gösterilmektedir.
Why Aggregates Exist
In Event Sourcing, an aggregate is the unit that owns business rules and the source of truth for state changes. It is not a database table — it is a cluster of objects treated as one for the purpose of consistency.
- An aggregate has a single root entity that the outside world talks to.
- Every change goes through the root, so invariants can be enforced in one place.
- The aggregate is the consistency boundary: everything inside is kept transactionally valid together.
In CQRS terms, the aggregate lives on the write side. It accepts commands, validates them, and emits domain events that describe what happened.
Commands vs Events
Two message types drive an event-sourced system, and confusing them is the most common modeling mistake.
- A command is an intent: a request to do something that may be rejected. Named imperatively:
OpenAccount,WithdrawFunds. - A domain event is a fact: something that already happened and cannot be rejected. Named in past tense:
AccountOpened,FundsWithdrawn.
The aggregate is the bridge: command in → validate → events out. Events are the only thing that gets persisted; state is derived from them.
// Commands express intent (may be rejected)
const openAccount = { type: 'OpenAccount', accountId: 'a-1', owner: 'Ada' };
const withdraw = { type: 'WithdrawFunds', accountId: 'a-1', amount: 50 };
// Events express facts (already happened)
const accountOpened = { type: 'AccountOpened', accountId: 'a-1', owner: 'Ada' };
const fundsWithdrawn = { type: 'FundsWithdrawn', accountId: 'a-1', amount: 50 };
console.log('command:', withdraw.type);
console.log('event:', fundsWithdrawn.type);Rebuilding State From Events
An event-sourced aggregate never stores its current state directly. Instead it folds over its past events to reconstruct state on load. This pure function is often called apply or the reducer.
apply(state, event)must be deterministic and side-effect free.- Replaying the same event stream always yields the same state.
- This is how you load an aggregate before handling a new command.
function apply(state, event) {
switch (event.type) {
case 'AccountOpened':
return { id: event.accountId, owner: event.owner, balance: 0 };
case 'FundsDeposited':
return { ...state, balance: state.balance + event.amount };
case 'FundsWithdrawn':
return { ...state, balance: state.balance - event.amount };
default:
return state;
}
}
const history = [
{ type: 'AccountOpened', accountId: 'a-1', owner: 'Ada' },
{ type: 'FundsDeposited', amount: 100 },
{ type: 'FundsWithdrawn', amount: 30 },
];
const state = history.reduce(apply, null);
console.log(state); // { id: 'a-1', owner: 'Ada', balance: 70 }The Command Handler Shape
Command handling has a consistent shape on the aggregate:
- Decide: a pure function
(state, command) => events[]that validates invariants and returns the events to emit (or throws/returns an error). - Evolve: the
applyfunction from the previous scene, which folds events into state.
Keeping decide pure means no I/O, no clock, no randomness inside it — pass those in. This makes the core domain logic trivially unit-testable.
function decide(state, command) {
switch (command.type) {
case 'OpenAccount':
if (state) throw new Error('Account already exists');
return [{ type: 'AccountOpened', accountId: command.accountId, owner: command.owner }];
case 'WithdrawFunds':
if (!state) throw new Error('Account not found');
if (command.amount <= 0) throw new Error('Amount must be positive');
if (command.amount > state.balance) throw new Error('Insufficient funds');
return [{ type: 'FundsWithdrawn', accountId: state.id, amount: command.amount }];
default:
throw new Error('Unknown command: ' + command.type);
}
}
console.log(decide({ id: 'a-1', balance: 70 }, { type: 'WithdrawFunds', amount: 30 }));Enforcing Invariants Inside the Boundary
An invariant is a rule that must always hold true for the aggregate. The classic example: "an account balance may never go negative."
- Invariants are checked in
decidebefore any event is emitted. - If a command would break an invariant, no event is produced and the command is rejected.
- Because all state lives inside one consistency boundary, the check can be made against fully consistent data — no cross-aggregate reads needed.
This is the heart of why aggregate boundaries matter: they define exactly which data must be strongly consistent together.
Designing the Consistency Boundary
How big should an aggregate be? The rule of thumb: make it as small as possible while still being able to enforce its invariants in a single transaction.
- Data that must change together transactionally belongs in the same aggregate.
- Data that can be eventually consistent belongs in separate aggregates.
- One command should modify exactly one aggregate instance per transaction.
Example: an Order and its line items share an invariant (total = sum of lines), so they are one aggregate. A Customer and their Orders do not, so they are separate aggregates linked only by ID.
A Self-Contained Aggregate Module
Putting decide and apply together gives a complete, framework-free aggregate. Notice it depends on nothing external — it just transforms data.
loadrebuilds state from history.handleloads, decides, and returns new events for the infrastructure to persist.
function apply(state, e) {
switch (e.type) {
case 'OrderCreated': return { id: e.orderId, lines: [], placed: false };
case 'LineAdded': return { ...state, lines: [...state.lines, e.line] };
case 'OrderPlaced': return { ...state, placed: true };
default: return state;
}
}
function decide(state, cmd) {
switch (cmd.type) {
case 'CreateOrder':
if (state) throw new Error('exists');
return [{ type: 'OrderCreated', orderId: cmd.orderId }];
case 'AddLine':
if (!state || state.placed) throw new Error('cannot add line');
return [{ type: 'LineAdded', line: cmd.line }];
case 'PlaceOrder':
if (!state || state.lines.length === 0) throw new Error('empty order');
return [{ type: 'OrderPlaced', orderId: state.id }];
default: throw new Error('unknown');
}
}
const load = (history) => history.reduce(apply, null);
const handle = (history, cmd) => decide(load(history), cmd);
let stream = handle([], { type: 'CreateOrder', orderId: 'o-1' });
stream = stream.concat(handle(stream, { type: 'AddLine', line: { sku: 'X', qty: 2 } }));
console.log(handle(stream, { type: 'PlaceOrder' }));Optimistic Concurrency With Versions
Two commands can race against the same aggregate. Event Sourcing solves this with an expected version check at append time.
- Each aggregate stream has a version = number of events appended so far.
- When loading, you capture the current version.
- When appending new events, you tell the store "only succeed if the stream is still at that version."
If another writer got there first, the append fails and you reload-and-retry. This enforces the consistency boundary without locking.
// In-memory event store demonstrating optimistic concurrency
class EventStore {
constructor() { this.streams = new Map(); }
load(id) { return this.streams.get(id) || []; }
append(id, expectedVersion, newEvents) {
const current = this.load(id);
if (current.length !== expectedVersion) {
throw new Error(`Concurrency conflict: expected ${expectedVersion}, got ${current.length}`);
}
this.streams.set(id, current.concat(newEvents));
}
}
const store = new EventStore();
store.append('a-1', 0, [{ type: 'AccountOpened' }]);
try {
store.append('a-1', 0, [{ type: 'FundsDeposited', amount: 10 }]); // stale version
} catch (err) {
console.log(err.message);
}
store.append('a-1', 1, [{ type: 'FundsDeposited', amount: 10 }]); // correct version
console.log('events:', store.load('a-1').length);Idempotency and Command Deduplication
Clients retry. A network blip can cause the same command to arrive twice, and you do not want two FundsWithdrawn events from one withdrawal.
- Attach a command id (idempotency key) to each command.
- The aggregate (or a dedup layer) records which command ids it has already processed.
- A duplicate command yields zero new events instead of repeating the effect.
Combined with version checks, this gives you exactly-once effect even on an at-least-once delivery channel.
function decide(state, cmd) {
// state.processed tracks handled command ids
if (state && state.processed.includes(cmd.commandId)) {
return []; // already handled -> no new events
}
if (!state) {
return [{ type: 'Opened', commandId: cmd.commandId }];
}
return [{ type: 'Deposited', amount: cmd.amount, commandId: cmd.commandId }];
}
function apply(state, e) {
if (!state) return { balance: 0, processed: [e.commandId] };
return { balance: state.balance + (e.amount || 0), processed: [...state.processed, e.commandId] };
}
let history = [];
history = history.concat(decide(history.reduce(apply, null), { type: 'Open', commandId: 'c1' }));
const retry = decide(history.reduce(apply, null), { type: 'Deposit', amount: 5, commandId: 'c1' });
console.log('duplicate command emitted events:', retry.length); // 0Modeling Events for the Long Term
Events are stored forever, so their shape is a long-lived contract. Model them carefully.
- Name events as business facts in past tense, not CRUD verbs (
OrderShipped, notOrderUpdated). - Capture intent and meaning, not just the resulting state diff — "why" matters for projections later.
- Include a
version/schema marker so you can upcast old events when the shape evolves. - Keep events lean: only domain data, never transient infrastructure details.
// A well-modeled domain event with metadata + schema version
function priceReduced({ productId, oldPrice, newPrice, reason }) {
return {
type: 'ProductPriceReduced',
schemaVersion: 1,
occurredAt: '2026-06-10T10:00:00Z', // injected, not Date.now()
data: { productId, oldPrice, newPrice, reason },
};
}
console.log(priceReduced({
productId: 'p-9', oldPrice: 100, newPrice: 80, reason: 'clearance',
}));Wiring the Aggregate Into a Handler
Infrastructure orchestrates the pure core. A typical write-side flow in a Node.js backend:
- Load the event stream for the target aggregate id.
- Fold it with
applyto get current state and capture the version. - Run
decide(state, command)to get new events. - Append events with the expected version (optimistic concurrency).
- Publish the events so projections and other bounded contexts react.
The domain logic stays pure; only this thin handler touches the store. This separation is what makes CQRS aggregates testable and resilient.
async function handleCommand(store, bus, id, command) {
const history = await store.load(id);
const state = history.reduce(apply, null);
const version = history.length;
const newEvents = decide(state, command); // pure domain decision
if (newEvents.length === 0) return state; // idempotent no-op
await store.append(id, version, newEvents); // optimistic concurrency
for (const e of newEvents) await bus.publish(e);
return newEvents.reduce(apply, state);
}Quick Check
Test your understanding of aggregate design and consistency boundaries.
Recap
You learned how to model the write side of an event-sourced, CQRS system:
- Aggregates are consistency boundaries with a single root that owns business rules.
- Commands express intent and may be rejected; events are immutable past-tense facts and are the only persisted state.
- Core logic splits into a pure
decide(state, command) => eventsand a pureapply(state, event) => state; state is rebuilt by folding events. - Invariants are checked in
decidebefore emitting events, all within one boundary. - Size aggregates to be as small as possible while still enforcing their invariants in one transaction — one command touches one aggregate.
- Optimistic concurrency (expected version) and idempotency keys give safe, exactly-once writes without locks.
- Model events as durable, well-named, versioned contracts for the long term.
Sıkça Sorulan Sorular
“Toplamalar, Komutlar ve Alan Olayı Modellemesi” dersi ücretsiz mi?
Evet — “Toplamalar, Komutlar ve Alan Olayı Modellemesi” dersin tüm metni burada web'de ücretsiz olarak okunabilir. Etkileşimli olarak pratik yapmak (yerleşik kod editörü ve 7/24 yapay zeka koçu) ve Node.js Backend Development Bootcamp kursunun geri kalanını açmak için CoddyKit PRO'ya yükselt. Node.js Backend Development Bootcamp kursu toplamda 4 dersten oluşur.
“Toplamalar, Komutlar ve Alan Olayı Modellemesi” dersinde ne öğreneceğim?
Tutarlılık sınırlarını uygularken komutları doğrulayan ve alan olayları yayan toplamalar tasarlayın. Node.js Backend Development Bootcamp ile uygulamalı kodu tarayıcıda doğrudan çalıştırarak pratik yaparsın ve 7/24 yapay zeka koçu dersi çalışırken sorularını yanıtlar.
Node.js Backend Development Bootcamp öğrenmeye başlamak için deneyim gerekli mi?
Önceden deneyim gerekmez. CoddyKit'te Node.js Backend Development Bootcamp, başlangıçtan ileri seviyeye kadar yapılandırıldığı için buradan başlayabilir veya başından başlayıp kendi hızında ilerleme yapabilirsin. Bu, 4 dersinin 2. dersidir.
“Toplamalar, Komutlar ve Alan Olayı Modellemesi” dersi ne kadar sürer?
Çoğu CoddyKit dersi yaklaşık 5–10 dakika sürer. Her biri kısa ve etkileşimli olduğu için sabit ilerleme yaparsın ve web ile uygulama arasında tam olarak bıraktığın yerden devam edebilirsin.
Bu Node.js Backend Development Bootcamp dersinde kod yazıp çalıştırabilir miyim?
Evet. Her Node.js Backend Development Bootcamp dersi yerleşik bir kod editörü içerir, bu sayede tarayıcıda gerçek kod yazıp çalıştırabilir ve anlık yapay zeka geri bildirimi alırsın — yerel kurulum gerekli değildir.
Bu kursun tüm dersleri
- Gerçeğin Kaynağı Olarak Olaylar ve Yalnızca Eklenen Günlük
- Toplamalar, Komutlar ve Alan Olayı Modellemesi
- Okuma Modelleri ve Projeksiyonlar Oluşturma
- Anlık Görüntüler, Sürümleme ve Olay Şeması Evrimi