Pattern 1: Universal Event Log
Intent
Capture every meaningful interaction and event in a single, unified time-series log to create comprehensive organizational memory that enables pattern recognition, prediction, and learning.
Also Known As
- Event Sourcing Log
- Interaction History
- Organizational Event Stream
- Activity Log
- Audit Trail
Problem
Organizations lose intelligence because events are not systematically captured.
When Sarah sends a payment reminder, the system knows the reminder was generated but not: - Whether the email was delivered - Whether the recipient opened it - Whether they clicked any links - How long until they responded - Whether the reminder led to payment
This information exists somewhere (email server logs, payment processor records) but it's scattered, unstructured, and inaccessible for analysis.
Without comprehensive event capture: - Pattern recognition is impossible (no data to analyze) - Predictions cannot be validated (no outcomes to compare against) - System cannot learn (no feedback loop) - Institutional knowledge remains in human memory - "What happened?" questions are unanswerable
Context
When this pattern applies:
- Organization has repetitive processes with many interactions
- Relationships extend over time (not one-time transactions)
- Multiple communication channels exist (email, SMS, phone, in-person)
- Understanding behavior patterns would improve outcomes
- Historical data would inform better decisions
When this pattern may not be needed:
- Pure one-time transactions with no ongoing relationship
- Very small scale (<20 entities) where human memory suffices
- No desire to learn from past behavior
- Privacy concerns outweigh intelligence benefits
Forces
Competing concerns:
1. Comprehensiveness vs Storage Cost - Want to log everything that might be useful - But storage isn't free (though now very cheap) - Balance: Log all interactions, aggregate raw data when needed
2. Real-time Logging vs Performance - Want immediate logging for real-time decisions - But synchronous database writes slow down operations - Balance: Asynchronous logging with queue buffering
3. Structured vs Flexible - Want consistent structure for querying - But new event types emerge constantly - Balance: Core fields required, flexible metadata field (JSON)
4. Retention vs Compliance - Want infinite history for long-term learning - But regulations may require data deletion - Balance: Anonymization after retention period, aggregate statistics preserved
5. Privacy vs Intelligence - Want detailed behavioral data - But individuals deserve privacy - Balance: Aggregate patterns, explicit consent, allow opt-out
Solution
Create a single, unified event log table that captures all organizational interactions with:
Core principle: Every meaningful event generates exactly one log entry with: - What happened (interaction_type) - When it happened (interaction_timestamp) - Who it involved (entity references) - The outcome (result/status) - Context details (flexible metadata)
Key characteristics:
1. Append-only: Never update or delete log entries (events are facts) 2. Time-ordered: Every event has precise timestamp 3. Comprehensive: All interactions across all channels 4. Unified: Single table, not scattered across system 5. Queryable: Structured for efficient pattern analysis
Data Quality Dependencies
The accuracy of all intelligence patterns depends on the quality of logged events.
Poor data capture at the source leads to unreliable intelligence: - Incomplete events → Missing data for pattern analysis (Pattern 16: Cohort Discovery & Analysis) - Incorrect timestamps → Wrong temporal patterns (Pattern 19: Causal Inference) - Missing outcomes → Can't validate predictions (Pattern 26: Feedback Loop Implementation) - Inconsistent types → Confused categorization (Pattern 18: Cohort Analysis)
Form design directly impacts event quality. User confusion, validation errors, and abandonment create incomplete or biased event data. See Volume 3, Part II (Interaction Patterns) for form design patterns that ensure: - High completion rates (complete data) - Low validation error rates (accurate data) - Positive user experience (unbiased samples) - Clear field definitions (consistent data)
Remember: Garbage in, garbage out. Intelligence is only as good as the events you capture.
Structure
Database Schema
CREATE TABLE interaction_log (
-- Unique identifier
interaction_id INT PRIMARY KEY IDENTITY(1,1),
-- When it happened
interaction_timestamp DATETIME2 NOT NULL DEFAULT GETDATE(),
-- Who it involved
family_id INT NOT NULL,
student_id INT NULL, -- Optional, for student-specific events
-- What happened
interaction_type VARCHAR(100) NOT NULL,
interaction_category VARCHAR(50) NULL,
-- How it happened
channel VARCHAR(50) NULL, -- email, sms, phone, portal, in_person
-- The outcome
outcome VARCHAR(50) NULL, -- opened, clicked, completed, bounced, ignored
-- Additional context (flexible)
metadata NVARCHAR(MAX) NULL, -- JSON for event-specific details
-- Audit
created_by VARCHAR(100) DEFAULT 'system',
-- Foreign keys
CONSTRAINT FK_interaction_family FOREIGN KEY (family_id)
REFERENCES families(family_id)
);
-- Indexes for performance
CREATE INDEX IX_interaction_family ON interaction_log(family_id);
CREATE INDEX IX_interaction_timestamp ON interaction_log(interaction_timestamp);
CREATE INDEX IX_interaction_type ON interaction_log(interaction_type);
CREATE INDEX IX_interaction_category ON interaction_log(interaction_category);
Event Type Taxonomy
Communication events:
- email_sent, email_opened, email_clicked, email_bounced
- sms_sent, sms_delivered, sms_responded
- phone_call_made, voicemail_left, call_returned
Document events:
- document_generated, document_downloaded, document_printed, document_sent
Engagement events:
- portal_login, portal_page_viewed, portal_action_taken
- event_attended, event_rsvp, event_no_show
- volunteer_signup, volunteer_hours_logged, volunteer_canceled
Financial events:
- payment_received, payment_late, payment_plan_created
- invoice_sent, invoice_viewed, invoice_disputed
Academic events:
- attendance_recorded, absence_noted, tardiness_logged
- grade_entered, progress_report_generated
Lifecycle events:
- inquiry_received, trial_scheduled, trial_attended
- enrollment_completed, withdrawal_requested
Form Events as Intelligence Source
Form submissions, field changes, validation errors, and completion patterns are critical event sources that feed organizational intelligence.
Forms are often the primary way organizations capture structured data. Every form interaction represents a moment where human knowledge becomes machine-readable data (see Volume 3: Human-System Collaboration). These interactions contain valuable signals:
Form lifecycle events:
- form_started - User begins filling form (captures intent)
- form_viewed - User opens form but doesn't start (hesitation signal)
- form_saved - User saves partial progress (complexity signal)
- form_resumed - User returns to saved form (commitment signal)
- form_submitted - Complete form submission (success!)
- form_abandoned - User leaves without completing (friction signal)
Form interaction events:
- field_changed - User modifies field value
- Metadata: field_name, old_value, new_value, edit_count
- Pattern: Multiple edits suggest confusion or uncertainty
- validation_error - System detects invalid input
- Metadata: field_name, error_type, error_message, submitted_value
- Pattern: Repeated errors indicate poor UX or unclear requirements
- field_focused - User clicks into field (attention signal)
- Metadata: field_name, time_spent_seconds, previous_field
- Pattern: Long focus times suggest difficulty or missing guidance
- help_viewed - User clicks help icon or tooltip
- Metadata: field_name, help_type, help_content_id
- Pattern: Frequently viewed help indicates need for better field design
Why form events matter for organizational intelligence:
- Pattern Discovery (Pattern 16: Cohort Discovery & Analysis)
- Which fields cause most abandonment?
- Where do users get stuck?
-
What validation errors happen most often?
-
Predictive Intelligence (Pattern 12: Risk Stratification)
- Users who abandon at specific field → 80% never return
- Users who save progress → 65% completion rate vs 40% without save
-
Users with validation errors → 3x longer completion time
-
Continuous Improvement (Pattern 26: Feedback Loop Implementation)
- Field with high error rate → Improve validation messages
- Section with high abandonment → Add progressive disclosure
-
Help content frequently viewed → Rewrite field label for clarity
-
Data Quality Assurance
- Forms with low error rates produce high-quality intelligence data
- Poor form design → Incomplete/incorrect data → Bad predictions
- Form events reveal data quality issues at source
Example form event schema:
// User abandons enrollment form after struggling with guardian section
await logInteraction({
family_id: 12845,
interaction_type: 'form_abandoned',
interaction_category: 'enrollment',
channel: 'portal',
outcome: 'incomplete',
metadata: {
form_name: 'student_enrollment',
form_version: '2024.3',
completion_percentage: 62,
fields_completed: 18,
fields_total: 29,
time_spent_seconds: 847,
last_section: 'guardian_information',
validation_errors: 3,
help_views: 5,
edit_count: 23
}
});
Cross-volume integration:
See Volume 3, Pattern 24 (Webhooks & Event Streaming) for detailed implementation of form event generation and Volume 3, Part II (Interaction Patterns) for form design patterns that ensure high-quality data capture and positive user experience.
Implementation
Logging Function
// Core logging function - call from anywhere in application
async function logInteraction(eventData) {
const {
family_id,
student_id = null,
interaction_type,
interaction_category = null,
channel = null,
outcome = null,
metadata = {}
} = eventData;
// Validate required fields
if (!family_id || !interaction_type) {
throw new Error('family_id and interaction_type are required');
}
// Insert into database (async, non-blocking)
await db.query(`
INSERT INTO interaction_log (
family_id, student_id, interaction_type,
interaction_category, channel, outcome, metadata
) VALUES (?, ?, ?, ?, ?, ?, ?)
`, [
family_id,
student_id,
interaction_type,
interaction_category,
channel,
outcome,
JSON.stringify(metadata)
]);
}
Instrumentation Examples
Example 1: Document generation
async function generateEnrollmentPacket(familyId) {
// Generate the document (existing code)
const packet = await createDocumentFromTemplate(
familyId,
'enrollment_packet'
);
// Log the event
await logInteraction({
family_id: familyId,
interaction_type: 'document_generated',
interaction_category: 'enrollment',
outcome: 'completed',
metadata: {
document_type: 'enrollment_packet',
page_count: packet.pages,
template_version: '2024.2'
}
});
return packet;
}
Example 2: Email sending with open tracking
// Send email
async function sendPaymentReminder(familyId) {
const family = await getFamily(familyId);
const email = await composeEmail(family, 'payment_reminder');
const result = await emailService.send(email);
// Log send event
await logInteraction({
family_id: familyId,
interaction_type: 'email_sent',
interaction_category: 'financial',
channel: 'email',
outcome: result.success ? 'sent' : 'bounced',
metadata: {
template: 'payment_reminder',
subject: email.subject,
message_id: result.messageId
}
});
}
// Webhook handler for email opens
app.post('/webhook/email-opened', async (req, res) => {
const { messageId, openedAt } = req.body;
// Find original send event
const originalEvent = await db.query(`
SELECT family_id, interaction_timestamp
FROM interaction_log
WHERE JSON_VALUE(metadata, '$.message_id') = ?
`, [messageId]);
if (originalEvent) {
// Log open event
await logInteraction({
family_id: originalEvent.family_id,
interaction_type: 'email_opened',
interaction_category: 'financial',
channel: 'email',
outcome: 'engaged',
metadata: {
original_message_id: messageId,
time_to_open_hours: calculateHours(
originalEvent.interaction_timestamp,
openedAt
)
}
});
}
res.sendStatus(200);
});
Example 3: Payment recording
async function recordPayment(paymentData) {
const { family_id, amount, payment_date, due_date, method } = paymentData;
// Record in payments table (existing code)
const payment = await db.insertPayment(paymentData);
// Calculate if late
const daysLate = Math.max(0,
moment(payment_date).diff(moment(due_date), 'days')
);
// Log payment event
await logInteraction({
family_id: family_id,
interaction_type: 'payment_received',
interaction_category: 'financial',
channel: method, // 'online', 'check', 'cash'
outcome: daysLate === 0 ? 'on_time' : 'late',
metadata: {
amount: amount,
payment_id: payment.id,
days_late: daysLate,
payment_method: method
}
});
}
Query Examples
Recent activity for family:
SELECT
interaction_timestamp,
interaction_type,
interaction_category,
channel,
outcome
FROM interaction_log
WHERE family_id = 187
AND interaction_timestamp >= DATEADD(day, -60, GETDATE())
ORDER BY interaction_timestamp DESC;
Email engagement rate:
SELECT
f.family_id,
f.family_name,
COUNT(CASE WHEN il.interaction_type = 'email_sent' THEN 1 END) as emails_sent,
COUNT(CASE WHEN il.interaction_type = 'email_opened' THEN 1 END) as emails_opened,
COUNT(CASE WHEN il.interaction_type = 'email_opened' THEN 1 END) * 100.0 /
NULLIF(COUNT(CASE WHEN il.interaction_type = 'email_sent' THEN 1 END), 0)
as open_rate
FROM families f
LEFT JOIN interaction_log il ON f.family_id = il.family_id
WHERE il.interaction_timestamp >= DATEADD(day, -90, GETDATE())
GROUP BY f.family_id, f.family_name
HAVING COUNT(CASE WHEN il.interaction_type = 'email_sent' THEN 1 END) > 0
ORDER BY open_rate ASC;
Payment timing patterns:
SELECT
family_id,
AVG(CAST(JSON_VALUE(metadata, '$.days_late') AS INT)) as avg_days_late,
COUNT(*) as total_payments,
SUM(CASE WHEN outcome = 'on_time' THEN 1 ELSE 0 END) * 100.0 / COUNT(*)
as on_time_percentage
FROM interaction_log
WHERE interaction_type = 'payment_received'
AND interaction_timestamp >= DATEADD(year, -2, GETDATE())
GROUP BY family_id;
Variations
By Scale
Small organizations (<100 entities): - Single log table sufficient - No partitioning needed - Can keep all history forever (storage negligible)
Medium organizations (100-1,000 entities): - Consider table partitioning by year - Implement log rotation after 5+ years - May need read replicas for analytics
Large organizations (1,000+ entities): - Partition by date range (monthly or quarterly) - Use time-series database (TimescaleDB, InfluxDB) - Separate operational and analytical databases
By Domain
Homeschool co-op: - Family-centric (family_id primary) - Seasonal patterns (semesters) - Volunteer events important
Property management: - Unit-centric (unit_id and tenant_id) - Monthly patterns (rent cycles) - Maintenance events important
Medical practice: - Patient-centric (patient_id) - Appointment-centric - HIPAA compliance required (encryption, access controls)
By Technology Stack
Relational database (SQL): - Use table shown above - JSON column for metadata - Standard indexes
Document database (MongoDB):
{
_id: ObjectId(),
timestamp: ISODate(),
family_id: 187,
event_type: 'email_opened',
category: 'financial',
channel: 'email',
outcome: 'engaged',
metadata: {
template: 'payment_reminder',
time_to_open_hours: 2.3
}
}
Time-series database (TimescaleDB): - Optimized for time-range queries - Automatic partitioning - Continuous aggregates for common metrics
Consequences
Benefits
1. Pattern recognition becomes possible With comprehensive history, you can discover: - "Families who don't open first three emails typically withdraw" - "Payment reminders sent on Monday have 67% open rate vs 43% on Friday" - "Volunteers in first semester stay 3.2 years vs 1.8 years average"
2. Predictions can be validated Compare "What we predicted" to "What actually happened": - Martinez family: Predicted 91% withdrawal risk → They withdrew (correct) - Chen family: Predicted 73% payment delay → Paid on time (incorrect, model needs tuning)
3. System learns continuously Track effectiveness of every action: - Template A converts 23%, Template B converts 31% → Use Template B - 10-day reminder timing: 81% on-time payment vs 7-day: 73% → Use 10-day
4. Complete audit trail Answer accountability questions: - "When did we contact them about this issue?" - "Did they receive our email?" - "What actions did we take before they withdrew?"
5. Temporal queries enabled Reconstruct past state: - "What was this family's engagement score on September 15?" - "How many families were at risk in Q3 2024?" - "Show me all interactions with Smith family in June"
Costs
1. Storage overhead - 1 million events ≈ 500 MB to 1 GB - At $0.021/GB/month cloud storage = $0.50 to $1/month - Negligible for most organizations
2. Development effort - Must instrument every interaction point - One-time cost of 20-40 hours typically - Ongoing: add logging for new features
3. Performance impact - Each action generates database insert - Mitigated by asynchronous logging - Minimal if done properly (<10ms per log)
4. Privacy considerations - Comprehensive logging feels invasive to some - Requires clear privacy policy - Must honor deletion requests
5. Query complexity - Event-sourced queries more complex than current-state queries - "What's their current status?" becomes "What's most recent status event?" - Trade-off accepted for intelligence benefits
Sample Code
Complete logging system:
// logger.js - Centralized logging module
const db = require('./database');
const queue = require('./queue');
class InteractionLogger {
constructor(options = {}) {
this.async = options.async !== false; // Default true
this.validateEvents = options.validateEvents !== false;
}
async log(eventData) {
// Validate required fields
if (this.validateEvents) {
this._validate(eventData);
}
// Add timestamp if not provided
if (!eventData.interaction_timestamp) {
eventData.interaction_timestamp = new Date();
}
// Async logging (recommended)
if (this.async) {
await queue.add('log-interaction', eventData);
return { queued: true };
}
// Synchronous logging (use only when necessary)
return await this._insert(eventData);
}
_validate(eventData) {
if (!eventData.family_id) {
throw new Error('family_id is required');
}
if (!eventData.interaction_type) {
throw new Error('interaction_type is required');
}
}
async _insert(eventData) {
const result = await db.query(`
INSERT INTO interaction_log (
interaction_timestamp,
family_id,
student_id,
interaction_type,
interaction_category,
channel,
outcome,
metadata,
created_by
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`, [
eventData.interaction_timestamp,
eventData.family_id,
eventData.student_id || null,
eventData.interaction_type,
eventData.interaction_category || null,
eventData.channel || null,
eventData.outcome || null,
eventData.metadata ? JSON.stringify(eventData.metadata) : null,
eventData.created_by || 'system'
]);
return {
interaction_id: result.insertId,
success: true
};
}
// Query helpers
async getRecentActivity(familyId, days = 30) {
return await db.query(`
SELECT * FROM interaction_log
WHERE family_id = ?
AND interaction_timestamp >= DATE_SUB(NOW(), INTERVAL ? DAY)
ORDER BY interaction_timestamp DESC
`, [familyId, days]);
}
async getEventCount(familyId, eventType, days = 90) {
const result = await db.query(`
SELECT COUNT(*) as count
FROM interaction_log
WHERE family_id = ?
AND interaction_type = ?
AND interaction_timestamp >= DATE_SUB(NOW(), INTERVAL ? DAY)
`, [familyId, eventType, days]);
return result[0].count;
}
}
module.exports = new InteractionLogger();
Known Uses
Homeschool Co-op Intelligence Platform (2024-2025) - 100 families, 3 years of history - 250,000+ events logged - Enables payment risk prediction, withdrawal risk prediction - Discovery queries run weekly on full event history
Property Management System (hypothetical) - 500 units across 5 properties - Tenant interactions, maintenance events, financial transactions - Predicts tenant churn, maintenance costs
Medical Practice Management (hypothetical) - 2,000 patients - Appointment history, no-show patterns, recall compliance - Optimizes scheduling, reduces no-shows
Related Patterns
Prerequisites (must have first): - None - this is the foundation pattern
Complementary (work well together): - Pattern 2: Behavioral Graph Construction - builds on event log - Pattern 3: Multi-Channel Tracking - extends event log to all channels - Pattern 4: Interaction Outcome Classification - adds outcome taxonomy - Pattern 27: Event Sourcing - optimizes event log storage
Enabled by this pattern: - Pattern 6: Composite Health Scoring - requires historical interaction data - Pattern 11: Historical Pattern Matching - requires comprehensive event history - Pattern 16: Cohort Discovery & Analysis - mines the event log for discoveries - All predictive and discovery patterns depend on this foundation
Volume 3 (Input Layer) - Form Design Patterns: - V3 Pattern 6: Domain-Aware Validation - ensures quality data enters event log - V3 Pattern 6: Domain-Aware Validation - prevents invalid data at source - V3 Pattern 24: Webhooks & Event Streaming - implements form event generation - V3 Pattern 18: Audit Trail - complements event log for compliance - V3 Part II: Interaction Patterns - all form patterns affect event data quality
Alternatives: - Current-state-only database (traditional approach, loses history) - Scattered logs across systems (no unified analysis possible)
References
Foundational Concepts: - Fowler, Martin. "Event Sourcing." https://martinfowler.com/eaaDev/EventSourcing.html (2005). The definitive introduction to event sourcing patterns. - Vernon, Vaughn. Implementing Domain-Driven Design. Addison-Wesley, 2013. (Chapter 8 covers event sourcing and CQRS) - Kleppmann, Martin. Designing Data-Intensive Applications. O'Reilly, 2017. (Chapter 11: Stream Processing and Event Logs) - Richardson, Chris. Microservices Patterns. Manning, 2018. (Pattern: Event Sourcing)
Event Log Implementations: - Apache Kafka Documentation: https://kafka.apache.org/documentation/ (Production-grade distributed event log) - EventStore Documentation: https://www.eventstore.com/event-sourcing (Purpose-built event sourcing database) - AWS DynamoDB Streams: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Streams.html (Managed event stream service)
Time-Series Data: - TimescaleDB Documentation: https://docs.timescale.com/ (PostgreSQL extension for time-series events) - InfluxDB Documentation: https://docs.influxdata.com/ (Purpose-built time-series database)
Related Patterns in This Trilogy: - Pattern 3 (Multi-Channel Tracking): Specialized event tracking across channels - Pattern 11 (Historical Pattern Matching): Uses event log to find similar cases - Pattern 16 (Cohort Discovery & Analysis): Discovers patterns in event data - Pattern 26 (Feedback Loop Implementation): Measures outcomes from events - Pattern 27 (Event Sourcing): Full event sourcing architecture - Volume 3, Pattern 24 (Webhooks & Event Streaming): How forms generate events for this log - Volume 3, Pattern 18 (Audit Trail): User-facing view of event history
Best Practices: - "The Log: What every software engineer should know about real-time data's unifying abstraction" by Jay Kreps: https://engineering.linkedin.com/distributed-systems/log-what-every-software-engineer-should-know-about-real-time-datas-unifying - "Event Sourcing: What it is and why it's awesome" by Greg Young: https://www.youtube.com/watch?v=8JKjvY4etTY (Video)