Skip to content

Communication Scheduler

Orchestrates outbound call scheduling at scale. Accepts call schedules with configurable time windows, retry policies, and concurrency limits; stages contacts via JSON or CSV upload; dispatches outbound calls through Quartz JDBC jobs; and resumes after each call completes via Redis pub/sub.

Attribute Value
Image communication-services/communication-scheduler
Runtime Spring Boot 4.0.5 / Java 25
Namespace nexivo
Replicas 3
Database scheduled_communications (PostgreSQL)

Tech Stack

Component Technology
Framework Spring Boot 4.0.5
Language Java 25
Database PostgreSQL
Queuing Redis (ZSET + pub/sub)
Job Scheduling Quartz Scheduler (JDBC-backed)
Batch Import Spring Batch
Script Templating FreeMarker
HTTP Clients Feign → call-service, contact-service

Core Entities

CallSchedule

Field Type Notes
id Long Auto-increment PK
scheduleId UUID External identifier
name String Display name
status Enum DRAFT → SCHEDULED → RUNNING → PASSED → COMPLETED
outboundTrunkId UUID References Phone Number Service trunk
purpose String Call purpose description
script String FreeMarker template
greetings String Opening greeting
retryAttempts Integer Max retry count
retryInterval String ISO 8601 duration (e.g. PT10M)
scheduledAt OffsetDateTime Fire time
callingHoursStart LocalTime Daily window start
callingHoursEnd LocalTime Daily window end
timezone String IANA timezone identifier
concurrentCalls Integer 1–10 simultaneous calls
dispositionGroups List\<UUID> JSONB — disposition group filter
routeConfig JSONB { role, aiAgentId, teamId, transferable }
callAfterFormula JSONB Per-contact window start formula
callBeforeFormula JSONB Per-contact window end formula
respectDnb Boolean Honour Do Not Disturb flag

Relations: one-to-many CsvUpload, one-to-many ScheduledCall.

ScheduledCall

Field Type Notes
id UUID PK
contactId UUID Reference to contact
callSchedule FK Parent CallSchedule
variables Map\<String,String> JSONB — FreeMarker substitution values
attempts Integer Retry counter
status Enum SCHEDULED / IN_PROGRESS / DO_NOT_DISTURB / ANSWERED / MISSED / BUSY / FAILED
callAfter OffsetDateTime Computed window start
callBefore OffsetDateTime Computed window end
callId UUID Call ID returned by call-service
lastAttemptedAt OffsetDateTime Timestamp of last dispatch attempt

Inserts are idempotent (ON CONFLICT DO NOTHING).

CsvUpload

Field Type Notes
id UUID PK
callSchedule FK Parent CallSchedule
fileName String Original filename
fileContent byte[] Raw file bytes
createdAt OffsetDateTime Upload timestamp

REST API

CallSchedule — /call-schedules

Method Path Description
POST / Create a new call schedule
GET /{id} Retrieve schedule (detail projection)
GET / Search and paginate schedules
PUT /{id} Full update
DELETE /{id} Delete schedule (cascades to contacts)
GET /{id}/triggers List Quartz triggers for this schedule
POST /{id}/trigger Immediate manual trigger
DELETE /{id}/triggers Pause / unschedule all triggers
PATCH /{id}/schedule Update scheduledAt + timezone
PATCH /{id}/instructions Update script, purpose, greetings
PATCH /{id}/contact-list Update contact list identifier
GET /{id}/disposition-groups Get disposition group UUID list
PATCH /{id}/disposition-groups Replace disposition group list

Scheduled Calls — /call-schedules/{scheduleId}/scheduled-calls

Method Path Description
POST / Add contacts as JSON list
POST /uploads Upload CSV file (multipart)
POST /uploads/{uploadId}/submit Kick off Spring Batch import job
GET /uploads/{uploadId}/jobs/{jobExecutionId} Poll batch job status
GET / Paginated list of scheduled calls
DELETE /{scheduledCallId} Remove individual scheduled call
GET /{scheduledCallId}/script Render FreeMarker script with contact variables

Execution Flow

sequenceDiagram
    participant Client
    participant API as CallScheduleController
    participant Listener as CallScheduleChangeListener
    participant Quartz as Quartz JobService
    participant Job as CallScheduleJob
    participant Redis as Redis ZSET
    participant Feign as call-service (Feign)
    participant Sub as RedisCallEndListener

    Client->>API: POST /call-schedules
    API->>API: Persist CallSchedule (status=SCHEDULED)
    API->>Listener: CallScheduleChange event (after-commit)
    Listener->>Quartz: scheduleJob(scheduleId, scheduledAt, timezone)

    Note over Quartz: At fire time...

    Quartz->>Job: execute CallScheduleJob
    Job->>Redis: ZADD scheduled-calls:{scheduleId} (eligible calls)
    Note over Job,Redis: Windowed: callAfter<=now<callBefore<br/>Unrestricted: no window constraints
    Job->>Redis: ZPOPMIN N (concurrentCalls)
    loop For each popped call
        Job->>Feign: dispatchCall → call-service (outbound call)
        Feign-->>Job: callId
        Job->>Job: Update ScheduledCall: status=IN_PROGRESS, callId
    end

    Note over Sub: Call ends in call-service...

    Sub->>Sub: Receive Redis pub/sub call-end event
    Sub->>Sub: Publish ScheduledCallEnd event
    Sub->>Job: triggerCalls(scheduleId, 1)
    Job->>Redis: ZPOPMIN 1
    alt More calls in ZSET
        Job->>Feign: dispatchCall → call-service
    else ZSET empty + windowed calls exhausted
        Job->>Quartz: reschedule trigger to next callAfter time
    else ZSET empty + no more calls
        Job->>Job: Transition CallSchedule → COMPLETED
    end

Window Logic

  • callAfterFormula and callBeforeFormula compute per-contact OffsetDateTime windows at import time.
  • At job execution, windowed calls (where callAfter <= now < callBefore) are loaded into the ZSET first.
  • If the windowed queue is exhausted before the next window opens, the Quartz trigger is rescheduled to the earliest next callAfter time.
  • Unrestricted calls (no formula set) are loaded as a fallback when windowed calls are exhausted.

CSV Import (Spring Batch)

Setting Value
Chunk size 50
Skip limit 100
Skippable exceptions IllegalArgumentException, FeignException

Processing steps:

  1. Reader — reads CSV rows from stored CsvUpload.fileContent.
  2. Processor — maps CSV columns to a ScheduledCall; resolves the contact via ContactService (by phone or email) or creates a new contact; applies callAfter / callBefore formulas to compute the time window.
  3. Writer — batch-saves ScheduledCall records with ON CONFLICT DO NOTHING to ensure idempotency.

Job execution state is tracked in spring_batch_* tables. Status can be polled via GET /uploads/{uploadId}/jobs/{jobExecutionId}.


Script Rendering (FreeMarker)

GET /call-schedules/{scheduleId}/scheduled-calls/{id}/script renders the CallSchedule.script FreeMarker template for a specific contact.

The FreeMarker context receives:

  • contact — the resolved contact object
  • variables — the Map<String,String> stored on the ScheduledCall

On render failure the raw (unrendered) template string is returned, so the API never errors on a template syntax issue.


Key Configuration

Property Description
spring.datasource.url jdbc:postgresql://…/scheduled_communications
spring.quartz.job-store-type jdbc
spring.quartz.properties.org.quartz.jobStore.tablePrefix public.qrtz_
spring.quartz.properties.org.quartz.jobStore.driverDelegateClass PostgreSQLDelegate
Quartz HikariCP pool max 5 (dedicated pool, separate from app pool)
csv.batch.chunk-size 50
csv.batch.skip-limit 100