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:

  1. Pattern Discovery (Pattern 16: Cohort Discovery & Analysis)
  2. Which fields cause most abandonment?
  3. Where do users get stuck?
  4. What validation errors happen most often?

  5. Predictive Intelligence (Pattern 12: Risk Stratification)

  6. Users who abandon at specific field → 80% never return
  7. Users who save progress → 65% completion rate vs 40% without save
  8. Users with validation errors → 3x longer completion time

  9. Continuous Improvement (Pattern 26: Feedback Loop Implementation)

  10. Field with high error rate → Improve validation messages
  11. Section with high abandonment → Add progressive disclosure
  12. Help content frequently viewed → Rewrite field label for clarity

  13. Data Quality Assurance

  14. Forms with low error rates produce high-quality intelligence data
  15. Poor form design → Incomplete/incorrect data → Bad predictions
  16. 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

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)