Event Sourcing מוסבר לעומק¶
בפוסט על CQRS נגענו ב-Event Sourcing בקצרה, אבל הנושא הזה שווה הרבה יותר מפסקה אחת. זו גישה שמשנה לגמרי את הדרך שבה חושבים על מסד נתונים, ולמי שמבין אותה לעומק היא פותחת יכולות שקשה להשיג אחרת - אבל גם מביאה איתה מורכבות אמיתית שכדאי להכיר לפני שקופצים פנימה.
הרעיון המרכזי - לא לשמור מצב, לשמור היסטוריה¶
ברוב האפליקציות, מסד הנתונים שומר רק את המצב הנוכחי. יש לכם שורה בטבלת accounts עם עמודת balance, ובכל פעולת הפקדה או משיכה אתם פשוט מעדכנים את המספר הזה. המצב הקודם נעלם ברגע שהעדכון קורה.
Event Sourcing הופך את זה על הראש - במקום לשמור את המצב, שומרים כל אירוע שהוביל אליו. במקום balance = 500, יש לכם רצף של אירועים: AccountOpened, MoneyDeposited(300), MoneyDeposited(400), MoneyWithdrawn(200). המצב הנוכחי הוא לא נתון ששמור איפשהו - הוא תוצאה מחושבת של הפעלת כל האירועים בסדר.
events = [
{"type": "AccountOpened", "amount": 0},
{"type": "MoneyDeposited", "amount": 300},
{"type": "MoneyDeposited", "amount": 400},
{"type": "MoneyWithdrawn", "amount": 200},
]
def rebuild_balance(events):
balance = 0
for event in events:
if event["type"] == "MoneyDeposited":
balance += event["amount"]
elif event["type"] == "MoneyWithdrawn":
balance -= event["amount"]
return balance
מסד הנתונים במקרה כזה נקרא event store, והוא לרוב append-only - כלומר, אף פעם לא מוחקים או משנים אירוע שכבר נכתב, רק מוסיפים אירועים חדשים. זה בעצמו יתרון אדיר מבחינת אמינות, כי אין דרך "לאבד" נתון בטעות עם עדכון שגוי.
למה בכלל להתאמץ - היתרונות האמיתיים¶
- ביקורת מלאה (audit log) בחינם. כל שינוי שאי פעם קרה במערכת מתועד באופן טבעי, בלי צורך לבנות מנגנון לוגים נפרד. במערכות פיננסיות או רגולטוריות זה שווה המון.
- שחזור מצב בכל נקודת זמן. רוצים לדעת מה היה מצב החשבון לפני חודש? מריצים את כל האירועים עד לאותו רגע ומקבלים תשובה מדויקת.
- ניפוי שגיאות אמיתי. כשמשהו משתבש בייצור, יש לכם רצף מדויק של מה שקרה, לא רק תמונת מצב סופית שלא מסבירה איך הגענו אליה.
- בסיס טבעי לאינטגרציה. אירועים אפשר לשדר הלאה למערכות אחרות בקלות, מה שמתחבר טבעי לארכיטקטורה מונעת אירועים.
האתגר האמיתי - קריאת המצב הופכת יקרה¶
הבעיה הברורה - אם חשבון בנק פעיל עשר שנים ויש לו מיליון אירועים, לחשב את היתרה הנוכחית על ידי הרצת כל האירועים בכל פעם זה בזבזני ואיטי. הפתרון המקובל הוא snapshot - כל כמה מאות אירועים, שומרים "תמונת מצב" של המצב המחושב עד כה. בפעם הבאה שצריך לחשב מצב, מתחילים מה-snapshot האחרון ומריצים רק את האירועים שאחריו, לא את כל ההיסטוריה מההתחלה.
אתגר נוסף, פחות מדובר, הוא שינוי סכמה. מה קורה כשמגלים שאירוע MoneyDeposited צריך שדה נוסף שלא היה קיים לפני שנתיים? אי אפשר פשוט "לתקן" אירועים ישנים, כי כל הרעיון הוא שהם קבועים לנצח. הפתרון הנפוץ הוא upcasting - קוד שיודע להמיר גרסה ישנה של אירוע לגרסה החדשה בזמן קריאה, בלי לגעת בנתונים המקוריים שנשמרו.
מתי זה באמת משתלם¶
Event Sourcing הוא כלי חזק לבעיה ספציפית, לא ברירת מחדל. הוא משתלם במערכות שבהן היסטוריה מדויקת היא דרישה עסקית או רגולטורית אמיתית - מערכות פיננסיות, רפואיות, או כל דבר שצריך "מה בדיוק קרה ומתי". הוא גם משתלם כשהמערכת כבר משתמשת בארכיטקטורה מונעת אירועים ממילא, ואז זה בעצם הרחבה טבעית של מה שכבר קיים.
לעומת זאת, אם אתם בונים מערכת CRUD רגילה בלי צורך אמיתי בהיסטוריה מלאה, Event Sourcing יוסיף לכם מורכבות עצומה - upcasting, snapshots, event store ייעודי - בתמורה ליתרון שאף אחד לא ביקש. זה בדיוק המקרה שבו כדאי לעצור ולשאול את עצמכם אם אתם פותרים בעיה אמיתית או רק מתלהבים מרעיון מגניב.
לסיכום¶
Event Sourcing הוא שינוי חשיבה - במקום לשמור מצב נוכחי, שומרים את כל האירועים שהובילו אליו, ומחשבים את המצב על פי דרישה. זה נותן ביקורת מלאה, שחזור היסטורי, וניפוי שגיאות ברמה שקשה להשיג אחרת, אבל בתמורה למחיר אמיתי של מורכבות בקריאה ובניהול שינויי סכמה. כמו כל כלי חזק, השאלה הראשונה היא לא "איך מיישמים את זה" אלא "האם יש לי באמת בעיה שדורשת אותו".
בקורס ארכיטקטורת תוכנה שלנו אנחנו בונים event store פשוט מאפס ועוברים על snapshots ו-upcasting בדוגמאות קוד מלאות.
מתלבטים אם Event Sourcing מתאים למערכת שלכם? הצטרפו לקהילה בדיסקורד ותשתפו את המקרה שלכם.