Back to Systems

Call Journaling Platform

Real-time call logging system that captures, processes, and journals every customer interaction across multiple channels.

The Problem

Customer-facing teams were logging calls by hand — copying numbers into spreadsheets, typing notes from memory minutes after a conversation ended. Data was scattered across personal files, shared drives, and sticky notes. When a customer called back, no one had the full picture. Duplicate entries were common, follow-ups fell through the cracks, and there was zero analytical visibility into call volume, resolution times, or recurring issues.

The business needed a single source of truth for every customer interaction, and they needed it without asking agents to change how they worked.

Constraints

The existing phone system was a legacy PBX setup with limited integration options — it could fire basic HTTP callbacks on call events, but the payload was minimal and unreliable. Call volume peaked at several hundred concurrent calls during business hours, so any solution had to handle bursts without dropping events. Most critically, the journaling had to feel real-time: supervisors wanted a live dashboard showing active and recently completed calls.

Solution Architecture

I designed a pipeline that treated every call event as an immutable message flowing through four stages:

  1. Webhook Receiver — A lightweight Laravel endpoint that accepted raw call events from the PBX, validated their signature, and immediately dispatched them onto a Redis-backed queue. The endpoint did almost no processing, keeping response times under 50ms to avoid PBX timeouts.

  2. Queue Processor — Dedicated Laravel queue workers picked up events and normalized the inconsistent PBX payloads into a canonical format. Deduplication logic keyed on call session IDs prevented double-processing when the PBX retried deliveries.

  3. Enrichment Service — The normalized event was enriched with customer context pulled from the CRM: account history, previous interactions, assigned representative. This step ran as a separate queued job so CRM latency never blocked the core pipeline.

  4. Storage & Broadcast — The enriched record was persisted to PostgreSQL with full indexing for search and reporting. Simultaneously, a WebSocket broadcast via Laravel Echo pushed the event to the supervisor dashboard in real time.

Tech Stack

  • Backend: Laravel 10, PHP 8.2
  • Queue & Cache: Redis with Laravel Horizon for worker management
  • Database: PostgreSQL with composite indexes on call metadata
  • Real-time: Laravel Echo + Pusher-compatible WebSocket server
  • Monitoring: Horizon dashboard, custom health checks on queue depth

What I Learned

This project taught me to think about backpressure before it becomes a problem. Early on, a CRM outage caused the enrichment queue to back up, which cascaded into memory pressure on the workers. I added circuit-breaker logic: if enrichment fails, the call record is stored with a flag and enriched later via a scheduled retry. The system degrades gracefully — you always get the call log, even if the CRM context arrives a few minutes late.

I also learned that the boring parts matter most. Deduplication, idempotent writes, and dead-letter handling aren’t exciting, but they’re the difference between a demo and a production system.