← Back
Home › Systems › Sales, marketing and customer service

marketing-automation

lifecycle messaging and campaigns

Braze, Customer.io

Open the marketing-automation pagemarketing-automation Roaring bitmapscompression: Compressed set of 32-bit integers in 64K-value chunks, each a sorted array, bitmap or run list; fast intersections, the standard for bitmap indexes.›Hdr histogramdata-structures: Log-linear bucketed histograms with bounded relative error for latency percentilesHyperLogLogdata-structures: Counts distinct elements in kilobytes with about 1 % error by tracking the longest run of leading zero bits in hashed values per bucket.›Deterministic simulationtestingdistributed: Run the whole system in one thread under a seeded scheduler with simulated network, disk and clock, so every failure replays from its seed.›Durable workflow executiondistributed: Ordinary code made crash-proof by logging every side-effect result and replaying the code from that log; activities run at least once, workflow logic must be deterministic.›Partitioned commit logdistributed: A topic split into replicated append-only partitions: key hash keeps per-key order, offsets give position, and a high watermark limits readers to fully replicated records.›Primary-backup replicationdistributed: One node orders all writes and ships them to standbys; the acknowledgement rule (none, one, all) sets what a crash loses, and an external arbiter decides who takes over.›Windowed stream aggregationdistributed: Group events by event-time window per key, keep one partial aggregate per slice, and emit a window when the watermark passes its end.›Idempotency keysfinance: Client-chosen unique key per logical operation, stored with a request fingerprint and the final response, so retries replay the first result instead of acting twice.›Process supervisionhardware-os: A parent process starts services, reaps their exits and restarts them under a delay and burst-limit policy, then stops them with SIGTERM before SIGKILL.›Multi-armed banditsmachine-learning: Choose among K options repeatedly while learning their payoffs: epsilon-greedy, UCB1 and Thompson sampling keep total regret low where a fixed split or a fixed exploration rate pays forever.›SPF, DKIM and DMARC senderauthenticationnetworking: SPF authorizes sending IPs, DKIM signs the message, DMARC requires either to match the visible From domain and says what to do on failure.›Email delivery smtpnetworking: Outbound SMTP: queues, MX lookup, retries, bounces, feedback loops and IP warm-upMobile push deliverynetworking: Sending notifications through APNs and FCM: HTTP/2 requests, token hygiene, collapse keys, TTL and priority, and the retry rules that keep you un-throttled.›Coverage-guided fuzzingsecurity: Mutate inputs, run an instrumented target, and keep any input that reaches a new code edge; feedback turns blind guessing into incremental search.›Mutual TLSsecurity: TLS where the client also proves a certificate key, so every connection carries a verified, rotatable machine identity.›A/B testing (online controlledexperiments)statistics: Randomly split units into arms by a salted hash, log exposure, compare metrics with a confidence interval, and guard the result with sample-ratio, guardrail and peeking checks.›Columnar storagestorage: Store each column contiguously so analytic scans read only the columns they need, compress well and run as tight SIMD loops.›Point-in-time recoverystorage: Periodic base backups plus a continuously archived change log let you rebuild the state as of any instant, then branch from there.›Vectorized executionstorage: Query operators pass batches of about 1-2K values per column instead of single tuples, amortizing interpretation overhead and enabling tight SIMD loops.›Calendar date arithmeticsystems: Civil dates, time zones (tzdb), DST gaps, month-end anchoring and duration mathExponential backoff with jittersystems: Retry after delays that grow geometrically and are randomized, so failed clients spread out instead of hammering a recovering service in synchronized waves.›Graceful drainingsystems: On a termination signal, stop taking new work, finish in-flight work until a deadline, tell clients to reconnect elsewhere, then exit; clients resume from a token.›Hierarchical timing wheelsystems: Timers bucketed by expiry into wheels of slots, coarser wheels for far deadlines: O(1) insert and cancel for millions of timeouts.›Sliding-window counterssystems: Count events in the last W seconds with a ring of sub-bucket counters; the two-bucket weighted estimate gives rate limits in 16 bytes per key.›Token bucketsystems: A bucket of up to b tokens refilled at rate r; requests spend tokens, enforcing an average rate while allowing bursts of b.›Cooperative schedulingconcurrency: Green threads/coroutines yielding at await points on a user-space schedulerFault injectiondistributed: Inject latency, errors, kills and partitions via proxies, netem or hooksRecord replaydistributed: Record every nondeterministic input (time, randomness, I/O, model and tool outputs, per-tick player input) and re-execute deterministically for debugging, replays and state rebuildEvent sourcingfinance: Persist state changes as an append-only stream of immutable events per entity; current state is a fold over them, guarded by expected-version appends.›Consumer offsetsdistributed: Each consumer group stores one integer per partition, the next position to read, so a restarted consumer resumes where it left off with at-least-once delivery.›Segmented logdistributed: Split an append-only log into files named by their first offset, write only the newest, and find any record by binary search plus a sparse index.›Zero-copy I/Onetworking: Move bytes between files, sockets and NICs by passing page references instead of copying through user space: sendfile, splice, MSG_ZEROCOPY, kTLS.›Health checkingsystems: Decide per backend whether to route to it using probes and real-traffic errors, with consecutive-count hysteresis, capped ejection and a panic fallback.›Fast hash functionsdata-structures: Non-cryptographic hashes built from 64×64→128-bit multiplies and xor-shifts; pick by key size, trust level and whether the hash is persisted.›Leasesdistributed: A lock that expires on its own: bounded-time ownership that survives crashes and partitions, given bounded clock drift and fencing tokens on writes.›Watermarksdistributed: Event-time progress markers that bound lateness and trigger window resultsHash aggregationstorage: GROUP BY via hash tables with partitioning and spill to disk+ 29 moreTransparent interceptionnetworking: Redirect traffic to a local proxy (iptables TPROXY/REDIRECT, eBPF) without app changesSyscall interceptionsecurity: Intercept syscalls via ptrace, seccomp user notify or a user-space kernelOptimistic concurrency controlstorage: Run transactions without locks, record what they read, and validate at commit that nothing changed; fastest when conflicts are rare.›Message sequence numbersdistributed: A per-stream counter on every message lets the receiver detect loss, duplicates and reordering, ack cheaply and resume after reconnect with "send from N".›Fencing tokensdistributed: A number that grows with every lock grant and is checked by the protected resource, so a paused ex-holder's late write is rejected.›d-ary heapdata-structures: Array heap with d children per node (d = 4 typical): half the binary heap's depth, siblings in one cache line, faster priority queues.›+ 17 moreEbpfhardware-os: Verified in-kernel programs on hooks (kprobes, XDP, tc) with mapsPtrace process controlhardware-os: Ptrace: attach, read and write registers and memory, single-step, intercept signalsSeccomp bpfsecurity: Syscall filtering with seccomp BPF programs+ 10 more+ 1 more

How it is built

Architecture overview

Marketing automation sends the right message to the right person at the right time and measures what happened. It keeps a store of contacts with their attributes and behaviour, groups them into segments (rules over that data), and runs campaigns or journeys: graphs of steps such as "wait two days", "if the email was opened", "send a push", "add to a segment". Every step that reaches a person goes through a delivery channel (email over SMTP, mobile push, SMS or webhook) that has to authenticate itself, respect rate limits and process bounces and unsubscribes. Every reaction (delivery, open, click, unsubscribe, purchase) returns as an event that updates the contact and may move it along the journey. The idea that makes it work is that segments and journeys are both functions over one event stream plus a contact snapshot: a segment is a stored query, a journey is a durable state machine per contact, and both are re-evaluated when an event arrives or a timer fires.

Parts:

 web/app SDK ─┐                                        ┌─► segment store ◄─ import / CRM sync
 email events ├─► ingest ─► event log ─► aggregates ───┤        │ membership changes
 push receipts│    (auth)   (per-contact order)        │        ▼
 webhooks ────┘                       │                └─► journey engine ◄── timers (wheel)
                                      │                        │ "send step"
                                      ▼                        ▼
                                 reports / A-B        send queue ─► rate limiter ─► channel workers
                                                      (idempotent)                  ├─► SMTP (DKIM-signed)
                                                                                    ├─► push gateway
                                                                                    └─► webhook / SMS
                       bounces, unsubscribes, opens, clicks ◄──────────────────────┘ (events re-enter)

How the parts fit together

Contact and segment store

A contact is a row of typed attributes plus a stream of events. A segment is a boolean filter over both, and membership has to be known fast for two jobs: "who is in this segment now" (a send) and "did this event just change anyone's membership" (a trigger). Columnar storage with Vectorized execution suits scans such as "country = X and last open within 30 days"; dictionary-encoding (no page yet) and zone-maps (no page yet) keep low-cardinality columns and date ranges cheap, and Delta encoding with Bit packing and PFOR shrink ids and timestamps. The membership of a stored segment is best held as a Roaring bitmap over dense contact ids, because "in A and not in B" then becomes bitmap algebra, and a suppression list is just one more bitmap to subtract.

Small installations do not need any of this. Mautic's documentation describes segments as lists of contacts defined by filters on contact fields, behaviour and company fields, combined with And/Or, and says dynamic membership only changes when a scheduled command rebuilds the segment (mautic:segments:update), while static segments are filled by hand, import, forms or campaign actions. That is the same contract with a batch rebuild instead of event-driven updates; the cost is that a contact can be in a stale segment between rebuilds. Event-driven maintenance needs the next part.

Event pipeline

Events are appended to a Partitioned commit log keyed by contact id, so all events of one person stay ordered and one consumer owns that person at a time. Segmented log gives cheap retention and replay, Consumer offsets let segments, journeys and reporting each read at their own pace, and Primary-backup replication keeps acknowledged events across a machine loss. The alternative named in the graph for the log is a Durable job queue; a queue is simpler when each event is consumed once, a log is better here because several consumers (segments, journeys, reports) need the same events and a late consumer must be able to rewind. Zero-copy I/O and Fast hash functions (for partition choice) matter only at high event rates.

Contract: an event has a contact id, a type, a timestamp from the source (not the receiver), and an idempotency key, so a retried webhook or a duplicated open pixel cannot count twice (Idempotency keys).

Real-time aggregates

Triggers such as "three site visits in a week" or "opened none of the last five emails" need counters. Use Windowed stream aggregation with watermarks (no page yet) to say when a window is complete despite late events (mobile apps upload events minutes or days late), and keep the running state in an LSM-tree, whose write-heavy pattern fits per-contact counter updates. Unique reach per campaign is a HyperLogLog, and per-contact send caps ("at most two marketing messages a day") are Sliding-window counters. Both trade exactness for constant memory; frequency caps usually need to be exact for the one contact and approximate is fine for reports.

Journey engine

A journey is a graph; each contact has a position in it. Durable workflow execution makes that position survive crashes by recording each decision as an event (Event sourcing) and replaying it to rebuild state; record-replay (no page yet) is the debugging side of the same idea. Its alternative in the graph is DAG-based batch execution, which is how a nightly "run the campaign for everyone" job works. The batch model is simpler and is what Mautic's documentation describes: the campaign update and trigger commands (mautic:campaigns:update, mautic:campaigns:trigger) run on a schedule and execute due events. The durable model reacts in seconds and scales per contact, but needs a log and a timer service.

"Wait three days" is a timer. With millions of contacts waiting, a Hierarchical timing wheel fires them in O(1) per timer, and a Slab allocator keeps the timer and state objects compact. Waits are defined in the contact's local calendar ("Tuesday 9 am local time"), which needs calendar-date-arithmetic (no page yet): daylight-saving gaps and per-contact time zones are the usual source of mis-timed sends.

The journey engine never sends directly. It writes a "send step" job with an idempotency key built from (journey, step, contact, iteration) into the send queue, which is the seam to the channels.

Email channel

The email-delivery-smtp block (no page yet) covers the whole path: build the message, sign it, look up the recipient's mail exchanger through DNS, open a TLS 1.3 connection, speak SMTP (smtp-protocol, no page yet; the base specification is RFC 5321), and classify the answer. Three things are specific to marketing mail:

  • Sender authentication. SPF, DKIM and DMARC prove the mail comes from the claimed domain. DKIM (RFC 6376) signs selected headers and the body with a key published in DNS; DMARC adds alignment between the visible From domain and the SPF or DKIM domain and tells receivers what to do on failure. The original DMARC RFC (7489) is marked obsolete and replaced by RFC 9989, 9990 and 9991, so new work should cite those.
  • One-click unsubscribe. RFC 8058 requires one List-Unsubscribe header with an HTTPS URI and a List-Unsubscribe-Post header whose content is List-Unsubscribe=One-Click, both covered by the DKIM signature. The receiving mail client then sends an HTTPS POST to that URI without user input, so the endpoint must identify recipient and list from the URI (with an unforgeable token), process the unsubscription, and not redirect. The unsubscribe endpoint is a write path into the segment store's suppression list, and it must be as available as the send path.
  • Bounces. bounce-processing (no page yet) turns delivery status notifications (RFC 3464 defines the format) and synchronous SMTP failures into hard bounces (suppress at once) and soft bounces (retry, then suppress after a limit). Mautic's documentation names a command that fetches monitored mail for bounce management (mautic:email:fetch); listmonk's site lists bounces among its analytics.

Push channel

Mobile push delivery is the same send-queue contract with a different transport: the provider gateways take HTTP/2 requests authenticated with a JWT or certificate, and return per-token results that invalidate stale device tokens. The failure handling is the same as for email.

Send discipline

Every channel worker pulls from the send queue through a Token bucket per sending identity (IP, domain, provider), because mailbox providers throttle new or suddenly busy senders and a burst gets the whole domain deferred. Temporary failures retry with Exponential backoff with jitter. A worker takes a Lease on a job so that a crash hands it to another worker, and the Idempotency key on the job means the lease timing cannot cause a second copy: at-least-once delivery plus idempotent sends looks like exactly-once to the recipient. listmonk's site describes the same shape, "multi-threaded, high-throughput, multi-SMTP e-mail queues" with sliding window rate limiting.

Experimentation

A subject-line or timing test assigns each contact to a variant with Deterministic bucketing (hash of contact id and experiment id, so the assignment is stable and needs no stored table) and Experiment layering keeps overlapping tests independent. Analysis uses Hypothesis testing with CUPED to cut variance, and bootstrap-confidence-intervals and sequential-hypothesis-testing (both no page yet) for ratio metrics and early stopping. When the goal is to send the best variant rather than to learn, Multi-armed bandits (with Bayesian rate estimation and a PRNG) shift traffic toward the winner as clicks come in. Choose A/B when the decision must be defensible and the metric is slow; choose the bandit when each send is cheap and the metric arrives within hours.

Operations

The contact store and event log are the system of record, so Point-in-time recovery (a Write-ahead log plus base backups in Object storage, recovery via ARIES recovery) must be able to restore to just before a bad import or a mistaken segment send. Process supervision, Health checking and Graceful draining keep long-running senders from dropping jobs on deploy; Mutual TLS secures internal calls between ingest, journey engine and channel workers. Deterministic simulation testing and Coverage-guided fuzzing target the two hardest correctness problems: the timer and retry logic of the journey engine, and the parsers (HTML templates, inbound bounce mail). Latency is recorded with hdr-histogram (no page yet) histograms for event-to-journey-step delay and for send-queue wait, because a triggered message (a cart reminder, a one-time code) loses its value when the tail grows, and the percentiles tell the send-discipline layer when to shed bulk sends to protect triggered ones.

Data and control flow

One triggered email.

  1. A purchase event arrives at ingest, is authenticated, deduplicated by its key and appended to the log partition of that contact. Cost: one log append; order of milliseconds.
  2. The segment consumer applies it to the contact snapshot and flips segment bits; the aggregate consumer updates windowed counters.
  3. The journey consumer sees the contact enter the segment that starts a journey, records "entered" as an event and schedules the first wait on the timing wheel.
  4. The timer fires; the engine checks frequency caps and suppression, picks the variant by bucketing, renders the template and enqueues a send job with its idempotency key.
  5. A channel worker leases the job, takes a token from the bucket for the sending domain, signs with DKIM, adds the RFC 8058 headers and hands the message to the receiving server over TLS.
  6. The outcome returns as an event (accepted, deferred, bounced); later opens, clicks and unsubscribes arrive the same way and go back to step 1. Most of the elapsed time is the wait itself, then the receiving server.

One bulk broadcast. A segment is frozen into a bitmap snapshot, split into slices, each slice enqueued as a batch of jobs, and workers drain them under the token buckets. The send rate, not the code, sets the duration: a million messages at 100 per second take about 2.8 hours (arithmetic, not a measurement), which is why warming a new sending domain gradually is part of the plan.

One recovery. Stop sends, restore to the timestamp before the fault, replay the event log from the stored offsets, then re-run suppression before resuming, so that people who unsubscribed in the lost window are not mailed again.

Scaling

  1. Sending reputation and provider rate limits bind first, long before CPU. Lever: more sending identities spread across domains and IPs, per-provider token buckets, gradual ramp-up.
  2. Segment evaluation grows with contacts times rules. Lever: columnar scans, bitmaps for stored segments, incremental maintenance on events instead of full rebuilds, caching of expensive behavioural filters.
  3. Timer and journey state grows with contacts in flight. Lever: partition by contact id so each partition owns its own wheel; compact state objects; move cold journeys to storage.
  4. Event ingest peaks when a campaign lands and everyone opens at once. Lever: more partitions, batching, acknowledging before aggregation.
  5. Experiment analysis is batch and the cheapest to defer.

Failure modes

FailureSymptomCauseFix
Duplicate sendsContact receives two copiesWorker crashed after SMTP accept but before marking done; retry without a keyIdempotency key on the job; record the outcome before acknowledging; accept rare duplicates over lost sends
Mail lands in spam or is rejectedOpen rate collapses; many 5xx at one providerSPF/DKIM/DMARC misaligned, missing one-click unsubscribe, sudden volume jump, high complaint rateAlign the domains, add RFC 8058 headers, ramp volume, suppress non-openers
Unsubscribe ignoredComplaints, legal exposureSuppression list read from a stale replica or segment cacheCheck suppression at enqueue and again at send time; unsubscribe writes are synchronous
Mis-timed sendsMessages at 3 am local time, or skipped around a clock changeServer-time waits; daylight-saving gap in a local-time waitStore zone per contact, compute fire time with calendar arithmetic, test gap/overlap days
Journey stuck or loopingContacts never exit; infinite re-entryLost timer after restart; condition that re-qualifies the same contactPersist timers in the event log; re-entry rules and a maximum step count per contact
Stale segmentWrong audience at send timeBatch rebuild (cron) is behind, or the rebuild lock is heldRebuild just before a broadcast, monitor rebuild age, move to event-driven maintenance
Bounce loopRepeated sends to dead addressesBounce mailbox not polled, unparsed reportsPoll the bounce channel, treat unparsable as soft, cap retries, suppress after repeats
Experiment shows false win"Winner" disappears after rolloutPeeking at fixed-horizon tests, overlapping tests, unequal bucket sizesSequential tests or fixed horizon, layering, check sample-ratio mismatch

Real implementations

namewhat it isrepolicencenotes
MauticOpen-source marketing automation: contacts, dynamic and static segments, campaigns, forms, emailhttps://github.com/mautic/mauticGPL-3.0-or-later (LICENSE.txt, checked)Batch design: cron commands rebuild segments, run campaign events and send queued mail, per https://docs.mautic.org/en/5.x/configuration/cron_jobs.html
listmonkSelf-hosted newsletter and mailing-list manager, also transactional mail; single binary plus Postgreshttps://github.com/knadh/listmonkAGPL-3.0 (LICENSE, checked)Lists, SQL-expression subscriber queries, SMTP queues with sliding-window rate limiting, bounce analytics; no journey builder described on https://listmonk.app
BrazeCommercial lifecycle messaging platformhttps://www.braze.comproprietarythe design above is the generic one
HubSpot Marketing HubCommercial marketing automation inside a CRMhttps://www.hubspot.com/products/marketing/marketing-automationproprietary

Sources

  • RFC 8058, Signaling One-Click Functionality for List Email Headers: https://www.rfc-editor.org/rfc/rfc8058
  • RFC 6376, DomainKeys Identified Mail (DKIM) Signatures: https://www.rfc-editor.org/rfc/rfc6376
  • RFC 7489, DMARC (obsolete); replacements RFC 9989 https://www.rfc-editor.org/rfc/rfc9989, RFC 9990 https://www.rfc-editor.org/rfc/rfc9990, RFC 9991 https://www.rfc-editor.org/rfc/rfc9991. Original: https://www.rfc-editor.org/rfc/rfc7489
  • RFC 5321, Simple Mail Transfer Protocol: https://www.rfc-editor.org/rfc/rfc5321
  • RFC 3464, Delivery Status Notifications: https://www.rfc-editor.org/rfc/rfc3464
  • Mautic documentation: campaigns https://docs.mautic.org/en/5.x/campaigns/campaigns_overview.html, segments https://docs.mautic.org/en/5.x/segments/manage_segments.html, cron jobs https://docs.mautic.org/en/5.x/configuration/cron_jobs.html
  • listmonk: https://listmonk.app

Similar systems

Share the most building blocks with this one.