איך מעצבים מערכת תשלומים - מקרה בוחן¶
מערכת תשלומים היא אחד המקומות הבודדים בעיצוב מערכות שבהם הכלל הרגיל מתהפך - ברוב המערכות מוכנים להקריב קצת עקביות בשביל ביצועים וזמינות, אבל בתשלומים, טעות של שקל אחד בכיוון הלא נכון היא בעיה אמיתית, לא רק אי נוחות. זה מקרה בוחן שמלמד לחשוב אחרת.
שלב 1 - הזרימה הבסיסית¶
בגבוה, זרימת תשלום נראית פשוטה - משתמש מבקש לשלם, המערכת פונה לספק סליקה חיצוני (כמו חברת אשראי או ספק תשלומים), הספק מאשר או דוחה, והמערכת מעדכנת את מצב ההזמנה בהתאם. הבעיה היא שבין הבקשה לתשובה עוברים כמה שלבים, וכל אחד מהם יכול להיכשל בנפרד - הרשת יכולה ליפול, הספק יכול להיתקע, והמערכת שלכם יכולה לקרוס בדיוק ברגע הלא נכון.
שלב 2 - מכונת מצבים לעסקה¶
הדרך הנכונה לחשוב על תשלום היא לא כפעולה בודדת שמצליחה או נכשלת, אלא כעסקה שעוברת בין מצבים ברורים - נוצרה, ממתינה לאישור ספק, אושרה, נכשלה, הוחזרה. כל מעבר בין מצבים נרשם, וחשוב מאוד - כל מעבר חייב להיות חוקי לפי הכללים שהוגדרו מראש. עסקה שכבר סומנה כ"אושרה" אסור שתעבור בטעות בחזרה ל"ממתינה", וזה בדיוק סוג הבאג שגורם לנזק כספי אמיתי אם לא נאכף בקפדנות ברמת הקוד ומסד הנתונים.
שלב 3 - אידמפוטנטיות - ההגנה הכי חשובה¶
מה קורה אם הבקשה לחיוב נשלחת פעמיים, למשל בגלל שהמשתמש לחץ פעמיים על כפתור התשלום כי החיבור התעכב? בלי הגנה, זה עלול לגרום לחיוב כפול. הפתרון הוא Idempotency Key - מזהה ייחודי שנוצר עבור כל ניסיון תשלום, כך שגם אם הבקשה מגיעה כמה פעמים, השרת מבצע את החיוב בפועל רק פעם אחת, ומחזיר את אותה תוצאה לכל הבקשות הכפולות. זה עיקרון שדיברנו עליו בעבר בהקשר כללי יותר של עיצוב API, אבל בתשלומים הוא לא המלצה - הוא דרישה בסיסית.
שלב 4 - מה קורה כשספק הסליקה לא עונה¶
זו נקודת הכשל הכי נפוצה בפועל. שלחתם בקשת חיוב לספק, וזמן ההמתנה עובר בלי תשובה - האם החיוב בוצע בפועל אצל הספק ורק התשובה לא הגיעה, או שהוא בכלל לא בוצע? אי אפשר לדעת בוודאות מיידית, וזו בדיוק הסיבה שאסור לשלוח בקשת חיוב חדשה באופן אוטומטי במקרה הזה - זה עלול ליצור חיוב כפול אצל הספק. הפתרון הנכון הוא לסמן את העסקה כ"במצב לא ברור", ולברר את הסטטוס האמיתי מול הספק בדרך אחרת - שאילתת סטטוס נפרדת שלא יוצרת חיוב חדש, או המתנה ל-webhook שהספק שולח בעצמו כשהוא מתאושש.
שלב 5 - לוג עסקאות בלתי ניתן לשינוי¶
עקרון מרכזי בעיצוב מערכות תשלומים הוא שרשומת העסקה עצמה, ברגע שנכתבה, לעולם לא נמחקת או משתנה - רק נוספים אליה רשומות חדשות. אם צריך לתקן משהו, לא עורכים את הרשומה הישנה, אלא יוצרים רשומת תיקון חדשה שמפנה אליה. זה נותן שני יתרונות - יש תמיד היסטוריה מלאה לביקורת (audit), ואי אפשר בטעות לאבד מידע על מה שקרה בפועל, גם אם מאוחר יותר מתגלה שהיה צריך לתקן משהו.
שלב 6 - הפרדה בין אישור לבין זיכוי בפועל¶
בהרבה מערכות תשלומים אמיתיות יש הפרדה בין "אישור" (authorization) - ספק הסליקה מאשר שיש כיסוי ושמותר לחייב - לבין "לכידה" (capture) - החיוב בפועל שקורה לפעמים בשלב מאוחר יותר. הפרדה כזאת מאפשרת גמישות עסקית, למשל לבטל הזמנה לפני שהכסף בפועל עבר, בלי לבצע החזר מלא שדורש תהליך נפרד ולפעמים יקר יותר.
מערכת תשלומים היא דוגמה מובהקת לכך שעיצוב מערכות טוב הוא לא רק על ביצועים ומדרגיות, אלא גם על נכונות ואמינות תחת כל תרחיש כשל אפשרי. בקורס ארכיטקטורת תוכנה שלנו עוברים על מקרי הבוחן האלה ומתרגלים לחשוב על כשלים לפני שהם קורים בייצור.
עבדתם על מערכת תשלומים ונתקלתם בתרחיש כשל מעניין? הצטרפו לקהילה בדיסקורד ותספרו איך פתרתם אותו.
לסיכום¶
מערכת תשלומים בנויה סביב מכונת מצבים ברורה לעסקה, אידמפוטנטיות שמונעת חיוב כפול, וטיפול זהיר במיוחד במצב שבו לא ברור אם ספק חיצוני ביצע את הפעולה בפועל. לוג עסקאות בלתי ניתן לשינוי ולוגית אישור-מול-לכידה נפרדת משלימים תמונה שמעדיפה בבירור נכונות על פני מהירות - ובצדק.