Вхід Частина 1 Ми обговорювали філософію та серію статей. Частина 1. 1 Мы создали образцовщик данных.
Тепер давайте побудуємо центральну систему.
Це зразок проекту (Mostlylucid.SegmentCommerceЦе навмисно спрощено, щоб продемонструвати концепції ясно, але це також включає справжні схеми інфраструктури (побудовані ззовні повідомлення, обробка завдань, індексування JSCB), які ви вимірюєте горизонтально у виробництві.
Думайте про це як про "продукти шаблонів в однофункціональному коефіцієнті." Та сама структура коду працює незалежно від того, чи ви працюєте один процес, чи розподіляєте дані за допомогою служб.
Ми обрали простий, з' єднаний стос щоб утримати зразок ясним, а потім демонструючи його моделі.
Замість окремих векторних баз даних, черг повідомлень та шарів кешування, ми використовуємо PostgreSQL:
flowchart LR
App[ASP.NET Core] --> PG[(PostgreSQL)]
PG --> JSONB[JSONB<br/>interests]
PG --> Vector[pgvector<br/>embeddings]
PG --> Queue[Queue tables<br/>jobs, outbox]
PG --> FTS[Full-text<br/>tsvector]
style PG stroke:#2f9e44,stroke-width:3px
style JSONB stroke:#1971c2,stroke-width:2px
style Vector stroke:#1971c2,stroke-width:2px
style Queue stroke:#1971c2,stroke-width:2px
style FTS stroke:#1971c2,stroke-width:2px
Один басейн з' єднання. Одна резервна копія.
<!-- Instant search (no page reload) -->
<input
hx-get="/api/search"
hx-trigger="keyup changed delay:300ms"
hx-target="#results" />
SPA- подібний до UX з простотою виконання серверів.
Шкала окремих програм, але шаблонів:
Розпочати просто. Розподіл за потреби.
Традиційні правила стеження за сховищами користувача, які можна розпізнати: імена, електронні листи, ІД користувачів, пов' язані з поведінкою. Правила GMR і конфіденційності роблять це все складнішим. Наш підхід до роботи відрізняється:
Ми зберігаємо поведінку, а не ідентичність.
flowchart LR
subgraph "Traditional Approach"
User1[User: john@email.com] --> Behavior1[Bought headphones]
Behavior1 --> PII1[(PII Database)]
end
subgraph "Zero-PII Approach"
User2[Anonymous Session] --> Behavior2[Bought headphones]
Behavior2 --> Pattern[(Category: tech<br/>Signal weight: 1.0)]
end
style PII1 stroke:#c92a2a,stroke-width:3px
style Pattern stroke:#2f9e44,stroke-width:3px
Головне розуміння: Вам не потрібно знати, хто така ВООЗ, щоб знати, в чому вони зацікавлені..
Система складається з трьох основних шарів:
flowchart TB
Browser[Browser] --> Session[Session Collector]
Session --> Profile[Persistent Profile]
Profile --> Segments[Segment Service]
Session -->|in-memory only| Cache[(IMemoryCache)]
Profile -->|elevated signals| DB[(PostgreSQL + JSONB)]
Segments -->|memberships| UI[Segment Explorer UI]
style Session stroke:#1971c2,stroke-width:3px
style Profile stroke:#2f9e44,stroke-width:3px
style Segments stroke:#fab005,stroke-width:3px
style Cache stroke:#e64980,stroke-width:2px
Критична різниця: Сеанси є ефемеральними. Профілі є постійними. Це не є докладними деталями } Це питання про архітектуру приватності.
Сеанси: строго в пам' ятіВони живуть у IMemoryCache з застарінням і ніколи не торкатись бази даних.
Це жорстке архітектурне обмеження: дані сеансів не можуть залишатися незмінними. Він збирає сигнали під час відвідування і виселення через 30 хвилин бездіяльності через правила кешування LFU (Стару часто використовувану) під тиском пам' яті.
// Mostlylucid.SegmentCommerce/Models/SessionProfile.cs
public class SessionProfile
{
public string SessionKey { get; set; } = string.Empty;
// Category interest scores: { "tech": 0.75, "fashion": 0.25 }
public Dictionary<string, double> Interests { get; set; } = new();
// Detailed signal counts: { "tech": { "product_view": 5, "add_to_cart": 1 } }
public Dictionary<string, Dictionary<string, int>> Signals { get; set; } = new();
// Products viewed this session
public List<int> ViewedProducts { get; set; } = new();
// Session context (device, referrer domain, time-of-day)
public SessionContext? Context { get; set; }
// Aggregates
public double TotalWeight { get; set; }
public int SignalCount { get; set; }
public int PageViews { get; set; }
public int ProductViews { get; set; }
public int CartAdds { get; set; }
// Timestamps
public DateTime StartedAt { get; set; } = DateTime.UtcNow;
public DateTime LastActivityAt { get; set; } = DateTime.UtcNow;
// Link to persistent profile (if fingerprint resolved)
public Guid? PersistentProfileId { get; set; }
}
// Mostlylucid.SegmentCommerce/Services/Profiles/SessionCollector.cs
_cache.Set(sessionKey, sessionProfile, new MemoryCacheEntryOptions
{
SlidingExpiration = TimeSpan.FromMinutes(30),
Priority = CacheItemPriority.Normal, // LFU eviction under memory pressure
// CRITICAL: Eviction callback decides whether to elevate to persistent profile
PostEvictionCallbacks =
{
new PostEvictionCallbackRegistration
{
EvictionCallback = async (key, value, reason, state) =>
{
if (value is SessionProfile session && ShouldElevate(session))
{
// Only NOW do we write to database (persistent profile)
await ElevateToProfileAsync(session);
}
// Otherwise: session is gone forever
}
}
}
});
Складні обмеження:
private bool ShouldElevate(SessionProfile session)
{
// Elevate if:
// - User added to cart (high intent)
// - User completed purchase (conversion)
// - Fingerprint was resolved (identity established)
// - Session weight exceeds threshold (engaged visitor)
return session.CartAdds > 0
|| session.TotalWeight > 5.0
|| session.PersistentProfileId.HasValue;
}
Навіщо виселятися?
Це лише безпечний пункт для визначення наполегливості. До часу, коли кеш викине сеанс:
Якщо ми не підвищимо рівень під час виселення, то частина: не завершено назавждиЦе і є план.
public class SessionContext
{
public string? DeviceType { get; set; } // "mobile", "desktop"
public string? EntryPath { get; set; } // "/products/tech" (no query params)
public string? ReferrerDomain { get; set; } // "google.com" (domain only, not full URL)
public string? TimeOfDay { get; set; } // "morning", "afternoon"
public string? DayType { get; set; } // "weekday", "weekend"
}
Зауважте, що неDescription of a condition. Do not translate key words (# V1S #, # V1 #,) тут: IP- адреси, агенти користувача, повні адреси URL, стеження за пікселями. Ми перехоплюємо контекстні шаблони, не розпізнана інформація.
А сигнал це поведінковий факт отриманий під час дії користувача. замість того щоб відстежувати "хто," ми стежимо "що сталося."
Ця концепція походить від ефемеральні сигналиУ цій системі дії випромінюють сигнали на кшталт: "api.rate_limited" або "gateway.slow" координувати поведінку без жорсткого зчеплення.
Тут ми застосовуємо той самий шаблон до поведінки користувача:
product_view сигнал (вагома: 0. 10)add_to_cart сигнал (вагома: 0, 5)purchase сигнал (вагома: 1. 00)Сигнали: ефемераль (доклади з сеансом), нуль- PI (без профілю) і вагований (нерозбірливо).
Для посилання сеансів без кук, ми використовуємо відбитки з боку клієнта. Переглядач обчислює хеш від сигналів (часовий пояс, роздільна здатність екрана, засіб для показу веб- GL, відбиток полотна) і відсилається тільки хеш до /api/fingerprint.
Тоді сервер HMACs, що хеш за допомогою секретного ключа, який робить його захищеним та непридатним для використання в інших місцях.
Цей код адаптується від мого Проект виявлення botТам, де вона використовується, щоб ідентифікувати скребків, ту ж техніку, іншу мету.
// Mostlylucid.SegmentCommerce/ClientFingerprint/fingerprint.js
// Collect signals (browser capabilities, not PII)
var signals = [
Intl.DateTimeFormat().resolvedOptions().timeZone,
navigator.language,
screen.width + 'x' + screen.height,
// ... (see full code)
];
// Hash locally
var hash = hash(signals.join('|'));
// Send only the hash via sendBeacon
navigator.sendBeacon('/api/fingerprint', JSON.stringify({ h: hash }));
Сторона сервера:
// Server HMACs the client hash with a secret key
var profileKey = HMACSHA256(clientHash + secretKey);
Тепер у нас є стабільний ідентифікатор сайту без печива або локальної Сторжування. повноцінне джерело відбитків відбитків. js (взято з maculid.bot-детекторації).
Різні дії мають різний рівень призначення. базові вагиunit synonyms for matching user input.
// Mostlylucid.SegmentCommerce/Data/Entities/Profiles/SignalEntity.cs
public static class SignalTypes
{
// Passive signals (low intent)
public const string PageView = "page_view"; // 0.01
public const string CategoryBrowse = "category_browse"; // 0.03
public const string ProductImpression = "product_impression"; // 0.02
// Active signals (medium intent)
public const string ProductView = "product_view"; // 0.10
public const string ProductClick = "product_click"; // 0.08
public const string Search = "search"; // 0.05
// High-intent signals
public const string AddToCart = "add_to_cart"; // 0.35
public const string AddToWishlist = "add_to_wishlist"; // 0.25
public const string ViewCart = "view_cart"; // 0.15
public const string BeginCheckout = "begin_checkout"; // 0.40
// Conversion signals (highest intent)
public const string Purchase = "purchase"; // 1.00
public const string Review = "review"; // 0.60
public const string Share = "share"; // 0.50
public static readonly Dictionary<string, double> BaseWeights = new()
{
{ PageView, 0.01 },
{ ProductView, 0.10 },
{ AddToCart, 0.35 },
{ Purchase, 1.00 },
// ... (see full code for complete list)
};
public static double GetBaseWeight(string signalType)
{
return BaseWeights.GetValueOrDefault(signalType, 0.05);
}
}
Чому це є важливим:
0.01) не буде домінувати сигнал0.35) є сильним сигналом наміру1.00) є найсильнішим сигналомЦя ієрархія забороняє " навігацію " за допомогою " забруднювати профіль.
// Mostlylucid.SegmentCommerce/Services/Profiles/SessionCollector.cs
public async Task<SessionProfile> RecordSignalAsync(
SessionSignalInput input, CancellationToken ct = default)
{
var sessionKey = input.SessionKey;
// Get or create session FROM CACHE (never DB)
var session = _cache.Get<SessionProfile>(sessionKey);
if (session == null)
{
session = new SessionProfile
{
SessionKey = sessionKey,
StartedAt = DateTime.UtcNow
};
}
session.LastActivityAt = DateTime.UtcNow;
var weight = input.Weight ?? SignalTypes.GetBaseWeight(input.SignalType);
// Update in-memory aggregates
session.TotalWeight += weight;
session.SignalCount++;
if (!string.IsNullOrEmpty(input.Category))
{
session.Interests.TryGetValue(input.Category, out var currentScore);
session.Interests[input.Category] = currentScore + weight;
}
if (input.SignalType == SignalTypes.AddToCart)
{
session.CartAdds++;
}
// Put back in cache with sliding expiration
_cache.Set(sessionKey, session, new MemoryCacheEntryOptions
{
SlidingExpiration = TimeSpan.FromMinutes(30),
Priority = CacheItemPriority.Normal,
PostEvictionCallbacks = { /* elevation callback */ }
});
return session;
}
Швидка через:
Коли сеанс показує високий намір (карт додає, купується), ми підносимо сигнали до постійний профіль.
// Mostlylucid.SegmentCommerce/Data/Entities/Profiles/PersistentProfileEntity.cs
[Table("persistent_profiles")]
public class PersistentProfileEntity
{
[Key]
public Guid Id { get; set; } = Guid.NewGuid();
[Required]
[MaxLength(256)]
public string ProfileKey { get; set; } = string.Empty;
// How this profile is identified (Fingerprint, Cookie, Identity)
public ProfileIdentificationMode IdentificationMode { get; set; }
// Behavioral data (all JSONB)
[Column("interests", TypeName = "jsonb")]
public Dictionary<string, double> Interests { get; set; } = new();
[Column("affinities", TypeName = "jsonb")]
public Dictionary<string, double> Affinities { get; set; } = new();
[Column("brand_affinities", TypeName = "jsonb")]
public Dictionary<string, double> BrandAffinities { get; set; } = new();
[Column("price_preferences", TypeName = "jsonb")]
public PricePreferences? PricePreferences { get; set; }
[Column("traits", TypeName = "jsonb")]
public Dictionary<string, bool> Traits { get; set; } = new();
// Computed segments
public ProfileSegments Segments { get; set; } = ProfileSegments.None;
[Column("llm_segments", TypeName = "jsonb")]
public Dictionary<string, double>? LlmSegments { get; set; }
// Vector embedding for similarity matching
[Column("embedding", TypeName = "vector(384)")]
public Vector? Embedding { get; set; }
// Statistics
public int TotalSessions { get; set; }
public int TotalSignals { get; set; }
public int TotalPurchases { get; set; }
public int TotalCartAdds { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime LastSeenAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}
Ще нуль PII:
ProfileKey є хеш HMAC (неможлива)IdentificationMode повідомляє нам про те, як його було визначено (fingerprint/cookie/login)public async Task ElevateToProfileAsync(
SessionProfileEntity session,
PersistentProfileEntity profile,
CancellationToken ct = default)
{
if (session.IsElevated)
return;
// Merge interests (use higher value)
foreach (var (category, score) in session.Interests)
{
if (!profile.Interests.ContainsKey(category) ||
profile.Interests[category] < score)
{
profile.Interests[category] = score;
}
}
// Update stats
profile.TotalSessions++;
profile.TotalSignals += session.SignalCount;
profile.TotalCartAdds += session.CartAdds;
profile.LastSeenAt = DateTime.UtcNow;
profile.UpdatedAt = DateTime.UtcNow;
// Mark session as elevated
session.IsElevated = true;
session.PersistentProfileId = profile.Id;
// Clear segment cache (will be recomputed)
profile.SegmentsComputedAt = null;
profile.EmbeddingComputedAt = null;
await _db.SaveChangesAsync(ct);
}
Коли відбувається підвищення:
Ми дражнили сегменти двох частин, а тепер доставимо їх. вивід придатного для дій З усіх цих сигналів колекція каже: "Що це за крамниця?"
Традиційна сегментація є бінарною: ви або у сегменті, або ні. Це створює проблеми:
flowchart LR
subgraph "Binary Segmentation"
Profile1[3 purchases] -->|"Threshold: 5"| Out[NOT High-Value]
Profile2[5 purchases] -->|"Threshold: 5"| In[High-Value]
end
Клієнт з 4 покупками сприймається однаково до 1 з 0. Це неправильно.
Невиразне сегментування надавати кожному профілю оцінку (0-1):
Дівчатаunit synonyms for matching user input |-----------|--------|-------------|
Тепер ви можете персоналізувати пропорційно: "майже високоцінні" клієнти отримують трохи інше лікування, ніж "ніде поруч."
// Mostlylucid.SegmentCommerce/Services/Segments/SegmentDefinition.cs
public class SegmentDefinition
{
public string Id { get; set; } // "tech-enthusiast"
public string Name { get; set; } // "Tech Enthusiasts"
public string Description { get; set; } // "Users with strong interest in technology"
public string Icon { get; set; } // "🔧"
public string Color { get; set; } // "#3b82f6"
public List<SegmentRule> Rules { get; set; } = [];
// How rules combine: All (AND), Any (OR), Weighted (sum)
public RuleCombination Combination { get; set; } = RuleCombination.Weighted;
// Minimum score to be "a member" (0-1)
public double MembershipThreshold { get; set; } = 0.3;
public List<string> Tags { get; set; } = []; // For filtering/grouping
}
Кожне з правил визначає один розмір профілю:
public enum RuleType
{
CategoryInterest, // Check interests.tech, interests.fashion, etc.
BrandAffinity, // Check brandAffinities.Sony, brandAffinities.Nike, etc.
PriceRange, // Check price preferences (budget vs luxury)
Trait, // Check boolean traits (prefersDeals, browsesExtensively)
Statistic, // Check totalPurchases, totalSessions, totalCartAdds
TagAffinity, // Check affinities.gadgets, affinities.organic, etc.
Recency, // Check days since last activity
Expression // Custom expressions (advanced)
}
public enum RuleOperator
{
GreaterThan, // value > threshold
GreaterOrEqual, // value >= threshold
LessThan, // value < threshold
LessOrEqual, // value <= threshold
Equal, // value == threshold
NotEqual, // value != threshold
Contains, // for array/string checks
Between, // for ranges
In, NotIn // for set membership
}
Об' єднати декілька правил з остаточним рахунком:
public enum RuleCombination
{
All, // AND logic: Score = min(all rule scores). All rules must pass.
Any, // OR logic: Score = max(all rule scores). Any rule can pass.
Weighted // Weighted sum: Score = Σ(rule.weight × rule.score) / Σ(rule.weight)
}
Вага Найбільш поширеним є те, що ви можете сказати: "категорія відсоткової ставки має значення 60%, послідовність має значення 30%, прив'язаність бренду має значення 10%."
Ось типові сегменти у проекті прикладу:
new SegmentDefinition
{
Id = "tech-enthusiast",
Name = "Tech Enthusiasts",
Description = "Users with strong interest in technology products",
Icon = "🔧",
Color = "#3b82f6",
MembershipThreshold = 0.35,
Rules =
[
new() {
Type = RuleType.CategoryInterest,
Field = "interests.tech",
Operator = RuleOperator.GreaterOrEqual,
Value = 0.4,
Weight = 0.6, // 60% of score
Description = "Tech interest > 40%"
},
new() {
Type = RuleType.TagAffinity,
Field = "affinities.gadgets",
Operator = RuleOperator.GreaterOrEqual,
Value = 0.2,
Weight = 0.2, // 20% of score
Description = "Likes gadgets"
},
new() {
Type = RuleType.TagAffinity,
Field = "affinities.electronics",
Operator = RuleOperator.GreaterOrEqual,
Value = 0.2,
Weight = 0.2, // 20% of score
Description = "Likes electronics"
}
]
}
Приклад обчислення:
interests.tech = 0.72, affinities.gadgets = 0.31, affinities.electronics = 0.15new SegmentDefinition
{
Id = "cart-abandoner",
Name = "Cart Abandoners",
Description = "Users who add items to cart but don't complete purchase",
Icon = "🛒",
Color = "#ef4444",
MembershipThreshold = 0.4,
Rules =
[
new() {
Type = RuleType.Statistic,
Field = "totalCartAdds",
Operator = RuleOperator.GreaterOrEqual,
Value = 3,
Weight = 0.5,
Description = "3+ cart adds"
},
new() {
Type = RuleType.Statistic,
Field = "totalPurchases",
Operator = RuleOperator.LessThan,
Value = 2,
Weight = 0.5,
Description = "Few purchases"
}
]
}
Це ставить "додається до возу, але не купується" шаблон, що представляє кампанії відновлення.
new SegmentDefinition
{
Id = "high-value",
Name = "High-Value Customers",
Description = "Customers who make frequent purchases and spend above average",
Icon = "💎",
Color = "#8b5cf6",
MembershipThreshold = 0.4,
Rules =
[
new() {
Type = RuleType.Statistic,
Field = "totalPurchases",
Operator = RuleOperator.GreaterOrEqual,
Value = 3,
Weight = 0.4,
Description = "3+ purchases"
},
new() {
Type = RuleType.PriceRange,
Field = "priceRange",
Value = "100-10000", // High-end shoppers
Weight = 0.3,
Description = "High price range"
},
new() {
Type = RuleType.Recency,
Field = "lastSeen",
Operator = RuleOperator.LessThan,
Value = 30,
Weight = 0.3,
Description = "Active in last 30 days"
}
]
}
new SegmentDefinition
{
Id = "bargain-hunter",
Name = "Bargain Hunters",
Description = "Price-sensitive shoppers who love deals and discounts",
Icon = "🏷️",
Color = "#22c55e",
MembershipThreshold = 0.3,
Rules =
[
new() {
Type = RuleType.PriceRange,
Field = "priceRange",
Value = "0-75", // Budget shoppers
Weight = 0.5,
Description = "Low price preference"
},
new() {
Type = RuleType.Trait,
Field = "traits.prefersDeals",
Value = true,
Weight = 0.3,
Description = "Prefers deals"
},
new() {
Type = RuleType.Statistic,
Field = "totalCartAdds",
Operator = RuleOperator.GreaterThan,
Value = 5,
Weight = 0.2,
Description = "Shops around"
}
]
}
У зразок міститься 10 сегментів зі спільними шаблонами електронного набору:
Д_ д. д. д. д. д. д. д. д. д. д. д. д. д. д. |---------|------|-----------|----------| Д-р Харріс: "Га-Ті-Ті-Ті-Ті-Ті-Ті - митниці" Центова ставка, технічний продукт. Д. д. д. д. д. д. д. д. ст. д. д. д. д. д. д. ст. Д-р Цукер: "Отже, ми маємо на увазі, що ми маємо на увазі, що ми маємо справу з іншими людьми." Нові сеанси, без закупівель, продається перший-пурчаний м'яч. Переводчики: music, music, music, music, music, music, music, music, music, music, music, multiple, music, music, music, multiple, minutes, availion, availue ♪ Home Encehiasts ♪ [Домівка] ♪ [Домівка] Д-р Цукер: "Отже, ми маємо на увазі, що ми маємо на увазі, що ми маємо справу з іншими людьми." Дівчата. 10+сеанси, +. Дівчата. Д_ д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д.
The SegmentService обчислює профілі за всіма правилами сегментів:
// Mostlylucid.SegmentCommerce/Services/Segments/SegmentService.cs
public SegmentMembership EvaluateSegment(ProfileData profile, SegmentDefinition segment)
{
var ruleScores = new List<RuleScore>();
foreach (var rule in segment.Rules)
{
var (score, actualValue) = EvaluateRule(profile, rule);
ruleScores.Add(new RuleScore
{
RuleDescription = rule.Description,
Score = score,
Weight = rule.Weight,
ActualValue = actualValue // For transparency
});
}
// Combine based on segment's combination method
double finalScore = segment.Combination switch
{
RuleCombination.All => ruleScores.Min(r => r.Score),
RuleCombination.Any => ruleScores.Max(r => r.Score),
RuleCombination.Weighted => ComputeWeightedScore(ruleScores),
_ => 0
};
return new SegmentMembership
{
SegmentId = segment.Id,
SegmentName = segment.Name,
Score = Math.Round(finalScore, 3),
IsMember = finalScore >= segment.MembershipThreshold,
RuleScores = ruleScores,
Confidence = score switch // Human-readable
{
>= 0.8 => "Very High",
>= 0.6 => "High",
>= 0.4 => "Medium",
>= 0.2 => "Low",
_ => "Very Low"
}
};
}
Кожен результат членства включає фактичні значення це призвело до результату:
// What the UI receives:
{
"segmentId": "tech-enthusiast",
"segmentName": "Tech Enthusiasts",
"score": 0.95,
"isMember": true,
"confidence": "Very High",
"ruleScores": [
{ "description": "Tech interest > 40%", "score": 1.0, "actualValue": "0.72" },
{ "description": "Likes gadgets", "score": 1.0, "actualValue": "0.31" },
{ "description": "Likes electronics", "score": 0.75, "actualValue": "0.15" }
]
}
Користувачі можуть точно зрозуміти чому Вони належать до сегменту, що є вирішальним для дотримання прозорості та стандарту ВВП.
Ось як перегляд продукту стає членом сегменту:
sequenceDiagram
participant Browser
participant Cache as SessionCache
participant Outbox as Outbox
participant Segment as SegmentService
Browser->>Cache: Product view (category: "tech")
Cache->>Cache: Update in-memory session
Note over Cache: interests.tech += 0.10
Browser->>Cache: Add to cart (high intent)
Cache->>Outbox: Publish elevation event
Outbox->>Outbox: Write to outbox table
Note over Outbox: Background worker processes
Outbox->>Segment: Elevate to PersistentProfile
Segment->>Segment: Evaluate segment rules
Note over Segment: interests.tech: 0.72 >= 0.40 ✓<br/>Score: 0.95, IsMember: true
Segment-->>Browser: Segment memberships + explanations
Всі важливі дії проходять крізь Шаблон outboxНаш основний механізм оркестрування:
flowchart LR
Action[Cart Add] --> TX[Single Transaction]
TX --> DB[(Save + Outbox)]
DB --> Worker[Background Worker]
Worker --> Route[Route to Handlers]
style TX stroke:#2f9e44,stroke-width:3px
Чому? Ділові дані та події записуються у одній операції. Події неможливо втратити. Невдачі автоматично повторюються з експоненціальним зворотним зв'язком.
// Every action publishes to outbox in the same transaction
await using var transaction = await _db.Database.BeginTransactionAsync(ct);
cart.Items.Add(new CartItem { ProductId = productId });
await _db.SaveChangesAsync(ct);
_outbox.Publish(OutboxEventTypes.ProductAddedToCart, new { ProductId = productId });
await _db.SaveChangesAsync(ct);
await transaction.CommitAsync(ct);
// Event is now guaranteed to be processed
Частина 3 містить повний опис реалізації вихідних повідомлень: черги завдань, LISTEN/ NOTIFY для негайного завантаження, повторення логіки і шаблонів масштабування.
Ця частина охоплює:
Частина 3 занурюється глибше:
SKIP LOCKED для розподіленої обробкиСегменти є виводом цієї системи, який придатний до дії. Вони відповідають "що це за крамниця?" з розмитими партитурами, а не бінарними відрами. І кожен користувач може точно бачити, чому він знаходиться у сегменті.
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.