system-design73 min read

How to Design a Small Business Email Platform like Constant Contact — A Senior+ Guide | Ayodhyya

How to Design a Small Business Email Platform like Constant Contact — A Senior+ Guide

Building event-driven email marketing, survey tools, and social posting for 600K+ small businesses

Published: October 3, 2024 Reading time: 55 min By Ayodhyya

1. Introduction — Constant Contact and the Small Business Email Landscape

Constant Contact has served as a cornerstone of digital marketing for small businesses since its founding in 1995. Acquired by Endurance International Group (now Newfold Digital) in 2015, the platform empowers over 600,000 small businesses to create professional email campaigns, manage events, build surveys, and maintain a social media presence — all from a single unified dashboard. The challenge of designing such a system is not merely about sending emails at scale; it is about building an ecosystem that makes sophisticated marketing tools accessible to users who have no marketing background and limited technical expertise.

At its core, Constant Contact solves a multi-sided problem. Small business owners need to reach their customers through email, but they also need to organize events, collect feedback through surveys, manage their contact lists intelligently, and amplify their message across social networks. Each of these capabilities involves distinct domain logic, yet they must be tightly integrated so that a single event registration can automatically update a contact list, trigger a confirmation email, and post to social media. This tight coupling between seemingly independent features is what makes the system design genuinely complex.

From an architectural perspective, the platform must handle massive throughput variations. A typical small business might send a few thousand emails per month, but during peak seasons — Black Friday, holiday campaigns, or local event promotions — the system might see a 50x spike in sending volume across the entire platform. The email sending pipeline must process millions of emails per hour during these peaks while maintaining high deliverability rates. Simultaneously, the event management system must handle real-time RSVPs, the survey builder must process responses asynchronously, and the social media scheduler must respect rate limits imposed by external platforms like Facebook, Instagram, and Google Business.

The data architecture behind Constant Contact is equally complex. Every account has its own contact lists, each with potentially hundreds of thousands of contacts. Campaign performance data — opens, clicks, bounces, unsubscribes — must be tracked in near real-time and aggregated into actionable reports. Event registrations, waitlists, check-in status, and payment processing must all be handled reliably. Survey responses must be stored efficiently and analyzed to produce insights. All of this data must be partitioned intelligently to ensure that no single account's operations degrade the experience for others on shared infrastructure.

Deliverability is perhaps the most critical non-functional concern. ISPs like Gmail, Yahoo, and Outlook employ sophisticated filtering algorithms that evaluate sender reputation, content quality, engagement patterns, and authentication records. A platform serving 600,000 businesses must maintain clean IP reputations, implement proper DKIM, SPF, and DMARC authentication, monitor blacklists proactively, and provide tools that help users write content that avoids spam filters. One compromised account sending phishing emails can damage the reputation of shared sending IPs, affecting deliverability for every other customer on the platform.

This system design guide walks through the complete architecture of building a Constant Contact-like platform from the ground up. We will cover the email campaign engine with its drag-and-drop editor, the event marketing system with RSVP tracking and reminders, the survey builder with logic branching, the social media integration layer, and all supporting infrastructure including contact management, campaign automation, landing pages, analytics, deliverability monitoring, and the multi-region deployment strategy. Every component is designed with production-grade reliability, horizontal scalability, and the specific needs of small business users in mind.

Who this guide is for: Senior engineers, staff architects, and engineering managers designing or scaling email marketing platforms. This guide assumes familiarity with distributed systems, message queues, and relational database design.

2. Functional and Non-Functional Requirements

Functional Requirements

The platform must support five primary functional domains, each with its own set of capabilities. Below we enumerate the critical features that define the system scope.

Email Campaign Management

  • Drag-and-drop email editor with pre-built templates for common use cases (newsletters, promotions, announcements)
  • Stock image library integration with thousands of free-to-use images
  • Mobile-responsive preview ensuring emails render correctly on all devices
  • Personalization tokens that pull contact attributes into email content
  • A/B testing for subject lines, send times, and content variants
  • Inline content blocks including text, images, buttons, social links, and HTML embeds
  • Template versioning allowing users to save, duplicate, and iterate on designs
  • Send scheduling with timezone-aware delivery for contacts across multiple regions

Contact Management

  • CSV import with column mapping and duplicate detection
  • Manual contact creation with custom field support
  • Automatic bounce processing removing invalid email addresses
  • Unsubscribe management with one-click opt-out and mandatory footer links
  • Contact segmentation based on demographics, engagement history, and custom fields
  • Auto-cleanup rules that archive contacts who have not engaged in N months
  • Contact scoring based on open rate, click rate, and interaction recency

Event Marketing

  • Event page creation with customizable branding, descriptions, and imagery
  • RSVP tracking with confirmed, tentative, and declined states
  • Waitlist management with automatic promotion when spots open
  • Automated reminder emails at configurable intervals before event start
  • Check-in functionality using QR codes or name search at the door
  • Ticketing and payment collection integration for paid events
  • Recurring event support for weekly meetups or monthly classes

Survey and Feedback Builder

  • Multiple question types: multiple choice, open text, rating scales, NPS, dropdown, and matrix
  • Logic branching that shows or hides questions based on previous answers
  • Response analytics with summary statistics, cross-tabulation, and export
  • Anonymous response collection option
  • Embeddable surveys for websites and shareable links
  • Conditional thank-you messages based on responses
  • Survey templates for common use cases like customer satisfaction and event feedback

Social Media Integration

  • Direct posting to Facebook pages, Instagram business accounts, and Google Business
  • Content scheduling with optimal time suggestions
  • Cross-posting from email campaigns to social networks
  • Basic engagement metrics aggregation from connected social accounts
  • Image and video attachment support for social posts

Non-Functional Requirements

RequirementTargetRationale
Availability99.95% uptimeSmall businesses depend on timely campaign delivery
Email Throughput10M emails/hour peakSeasonal spikes during holidays and promotions
Latency (Campaign Send)P99 < 2 seconds for API acknowledgmentImmediate feedback on send initiation
Latency (Dashboard Load)P95 < 800msResponsive user experience for non-technical users
Data DurabilityZero data loss for contacts and campaignsRegulatory and trust requirements
ScalabilityLinear horizontal scaling to 2M+ accountsGrowth target over 3 years
SecuritySOC 2 Type II compliant, GDPR readyEnterprise customer requirements and regulatory compliance
Deliverability> 98% inbox placement rateCore value proposition for customers

3. Capacity Estimation

Accurate capacity planning is essential for a platform that serves hundreds of thousands of small businesses. Each account generates distinct workload patterns across email sending, contact storage, event registration, and survey response collection. We must model these workloads independently and then combine them to produce aggregate infrastructure requirements.

Contact Storage

With 600,000 active accounts and an average of 2,500 contacts per account, the total contact store holds approximately 1.5 billion contact records. At an average of 1 KB per contact record including custom fields, this amounts to roughly 1.5 TB of raw contact data. Adding indexes, engagement history, and segmentation metadata, the total contact storage requirement is approximately 8-10 TB.

Email Volume

If each account sends an average of 5 campaigns per month with 2,500 recipients each, the platform processes approximately 7.5 billion emails per month. This breaks down to roughly 250 million emails per day, or approximately 3,000 emails per second on average. During peak periods like Black Friday, this could spike to 10,000-30,000 emails per second. With an average email size of 75 KB including HTML (images are served separately via CDN), the raw data throughput is approximately 225 MB/s average and up to 2.25 GB/s during peaks.

Event Registrations

Assuming 15% of accounts (90,000) host events and each event averages 50 registrations, the platform handles approximately 4.5 million event registrations per month. This translates to about 2 registrations per second on average, with burst traffic during popular event announcements reaching 50-100 registrations per second.

Survey Responses

If 20% of accounts (120,000) send surveys averaging 200 responses each, the platform collects approximately 24 million survey responses per month. At an average response payload of 2 KB, this represents about 48 GB of survey data per month, or roughly 1.6 GB per day.

MetricAveragePeakStorage
Active Accounts600,000650,000
Total Contacts1.5 billion1.8 billion~10 TB
Emails per Month7.5 billion30 billion (holiday)~50 TB logs
Emails per Second~3,000~30,000
Event Registrations per Month4.5 million20 million~2 TB
Survey Responses per Month24 million80 million~5 TB/year
Campaign Reports Generated3 million/day15 million/day~15 TB/year
Social Posts per Month1.2 million5 million~500 GB

Bandwidth Requirements

Combining all data flows — email rendering, asset delivery through CDN, API traffic, dashboard loads, and background processing — the platform requires approximately 10 Gbps of sustained egress bandwidth with burst capacity up to 40 Gbps during peak campaign sends. The CDN layer serving email images and landing page assets accounts for roughly 60% of total bandwidth.

Key Insight: The email sending pipeline is the primary throughput bottleneck. Design the queue architecture to handle 30,000 emails/second sustained, with backpressure mechanisms to prevent cascade failures during traffic spikes.

4. Data Model Design

The data model for a Constant Contact-like platform must balance normalization for data integrity with denormalization for query performance. The core entities span account management, contact storage, campaign definitions, event management, survey construction, and social media scheduling. Below we present the key entities and their relationships.

Account and User Entities

Every customer of the platform has an Account entity that represents their business subscription. Users are team members within an account, each with specific roles determining their permissions. The Account entity owns all downstream resources including contacts, campaigns, events, and surveys.

Contact and List Entities

Contacts are the fundamental data unit of the platform. A contact can belong to multiple lists, enabling segment-based targeting. Each contact maintains engagement history tracking every email open, click, bounce, and unsubscribe event. Custom fields extend the base contact schema to support business-specific attributes like membership type, purchase history, or preferred category.

Campaign Entity

A campaign represents a single email blast or automation. It contains the email content (HTML body, subject line, preview text), targeting criteria (which lists or segments to send to), scheduling information, and delivery status. Campaigns reference templates for their visual design and track performance metrics through separate analytics tables.

Event Entities

Events have a hierarchical structure: an Event contains multiple Occurrences (for recurring events), each Occurrence has a capacity limit and generates Registrations from contacts. Registrations flow through states from pending to confirmed, and may include waitlist entries. Check-in records are appended at event time to track actual attendance.

Survey Entities

Surveys consist of a Survey definition containing ordered Questions, each with a QuestionType and optional Choices. Logic Branches define conditional paths between questions based on answer values. Responses are stored as ResponseSets, each containing individual Answers linked to the originating contact (or anonymous if the survey is anonymous).

Social Post Entities

Social posts reference the source content (either an email campaign or original content), the target platforms (Facebook, Instagram, Google Business), scheduling information, and delivery status. Each connected social account is stored as a SocialConnection entity with OAuth tokens and platform-specific metadata.

Entity Relationship Diagram

erDiagram ACCOUNT ||--o{ USER : has ACCOUNT ||--o{ CONTACT_LIST : owns ACCOUNT ||--o{ CAMPAIGN : creates ACCOUNT ||--o{ EVENT : hosts ACCOUNT ||--o{ SURVEY : builds ACCOUNT ||--o{ SOCIAL_CONNECTION : connects CONTACT_LIST }o--o{ CONTACT : contains CONTACT ||--o{ CONTACT_ENGAGEMENT : generates CONTACT ||--o{ EVENT_REGISTRATION : registers CONTACT ||--o{ SURVEY_RESPONSE : submits CAMPAIGN }o--|| EMAIL_TEMPLATE : uses CAMPAIGN ||--o{ CAMPAIGN_METRIC : tracks CAMPAIGN ||--o{ SOCIAL_POST : cross_posts EVENT ||--o{ EVENT_OCCURRENCE : has EVENT_OCCURRENCE ||--o{ EVENT_REGISTRATION : receives EVENT_REGISTRATION ||--o{ EVENT_CHECKIN : records SURVEY ||--o{ SURVEY_QUESTION : contains SURVEY_QUESTION ||--o{ SURVEY_BRANCH : branches SURVEY ||--o{ SURVEY_RESPONSE : collects SURVEY_RESPONSE ||--o{ SURVEY_ANSWER : includes

Key Table Definitions

TablePrimary KeyPartition KeyEstimated Rows
accountsaccount_idaccount_id600,000
contactscontact_idaccount_id1.5 billion
contact_listslist_idaccount_id5 million
contact_list_memberscontact_id + list_idaccount_id3 billion
campaignscampaign_idaccount_id50 million
campaign_recipientsidcampaign_id30 billion
eventsevent_idaccount_id5 million
event_registrationsregistration_idevent_id500 million
surveyssurvey_idaccount_id2 million
survey_responsesresponse_idsurvey_id2 billion
email_eventsevent_idcampaign_id50 billion
social_postspost_idaccount_id10 million
Partitioning Strategy: Nearly every table is partitioned by account_id to ensure that all data for a single account resides on the same shard. This enables efficient multi-tenant queries and simplifies data isolation for compliance purposes.

5. API Design

The platform exposes a RESTful API that enables both the web frontend and third-party integrations to interact with all system capabilities. API design follows consistent conventions for authentication, pagination, error handling, and resource naming. All endpoints require an API key passed as a Bearer token in the Authorization header.

Campaign Endpoints

MethodEndpointDescription
POST/api/v1/campaignsCreate a new email campaign
GET/api/v1/campaignsList all campaigns with filtering and pagination
GET/api/v1/campaigns/{id}Get campaign details and current status
PUT/api/v1/campaigns/{id}Update campaign content or settings
DELETE/api/v1/campaigns/{id}Delete a draft campaign
POST/api/v1/campaigns/{id}/sendSchedule or immediately send the campaign
POST/api/v1/campaigns/{id}/duplicateCreate a copy of an existing campaign
GET/api/v1/campaigns/{id}/metricsGet real-time performance metrics

Contact Endpoints

MethodEndpointDescription
POST/api/v1/contactsCreate a single contact
POST/api/v1/contacts/bulkImport contacts from CSV or JSON array
GET/api/v1/contacts/{id}Get contact details and engagement history
PUT/api/v1/contacts/{id}Update contact attributes
DELETE/api/v1/contacts/{id}Remove a contact from the account
GET/api/v1/contacts/{id}/engagementGet full engagement timeline for a contact
POST/api/v1/contacts/segmentQuery contacts by segment criteria

Event Endpoints

MethodEndpointDescription
POST/api/v1/eventsCreate a new event with details and capacity
GET/api/v1/eventsList events with date range and status filters
PUT/api/v1/events/{id}Update event details
POST/api/v1/events/{id}/registerRegister a contact for an event
GET/api/v1/events/{id}/registrationsList all registrations with status
POST/api/v1/events/{id}/checkinCheck in a registrant at the event
GET/api/v1/events/{id}/reportGet event performance report

Survey Endpoints

MethodEndpointDescription
POST/api/v1/surveysCreate a new survey with questions
GET/api/v1/surveys/{id}Get survey definition and response summary
PUT/api/v1/surveys/{id}Update survey questions or settings
POST/api/v1/surveys/{id}/publishPublish survey and generate shareable link
POST/api/v1/surveys/{id}/responsesSubmit a survey response
GET/api/v1/surveys/{id}/analyticsGet response analytics and cross-tabulation
GET/api/v1/surveys/{id}/exportExport responses as CSV or Excel

Campaign Send Request Example

json
{
  "campaign_id": "cmp_8x7k2m9p",
  "send_config": {
    "mode": "scheduled",
    "scheduled_at": "2026-07-05T14:00:00Z",
    "timezone_behavior": "recipient_local",
    "batch_config": {
      "batch_size": 50000,
      "batch_interval_minutes": 15,
      "throttle_per_minute": 5000
    }
  },
  "targeting": {
    "list_ids": ["lst_abc123", "lst_def456"],
    "exclude_list_ids": ["lst_unsub01"],
    "segment_criteria": {
      "field": "last_opened_days",
      "operator": "less_than",
      "value": 90
    }
  },
  "ab_test": {
    "enabled": true,
    "variant_count": 2,
    "winner_metric": "open_rate",
    "sample_percentage": 20,
    "winner_send_delay_hours": 4
  }
}

Error Response Format

json
{
  "error": {
    "code": "INVALID_SEND_TIME",
    "message": "Scheduled send time must be at least 30 minutes in the future",
    "field": "send_config.scheduled_at",
    "documentation_url": "https://api.docs/errors/INVALID_SEND_TIME"
  }
}
API Versioning: All endpoints are versioned under /api/v1/. Breaking changes introduce a new version number. Deprecation notices are issued 6 months before any endpoint removal, and version negotiation is supported via the Accept header.

6. High-Level Architecture

The architecture of the Constant Contact platform follows a microservices pattern organized around domain boundaries. The system is composed of six primary service clusters: the Campaign Engine, the Event System, the Survey Builder, the Social Scheduler, the Contact Store, and the Analytics Pipeline. Each cluster communicates through asynchronous message queues for eventual consistency and through synchronous gRPC for low-latency queries requiring immediate responses.

The API Gateway serves as the single entry point for all client requests. It handles authentication, rate limiting, request routing, and response caching. The gateway delegates to the appropriate microservice based on the URL path and manages cross-cutting concerns like request logging and distributed tracing. The web frontend communicates exclusively through the API Gateway, while internal services communicate directly through service discovery.

The Campaign Engine is the highest-throughput component. When a user clicks "Send," the campaign is validated, personalization tokens are resolved, the recipient list is segmented, and individual send tasks are enqueued into a distributed message queue. Worker processes consume from the queue and hand off emails to the SMTP delivery layer, which manages connections to multiple downstream email service providers and ISP relay servers. Each worker maintains connection pools, implements retry logic with exponential backoff, and respects per-domain rate limits to protect sender reputation.

graph TB subgraph Client Layer WEB[Web Dashboard] MOB[Mobile App] API THIRD[Third-Party APIs] end subgraph Gateway Layer GW[API Gateway
Auth + Rate Limit] CDN[CDN
Static Assets + Images] end subgraph Application Services CAMPAIGN[Campaign Service] CONTACT[Contact Service] EVENT[Event Service] SURVEY[Survey Service] SOCIAL[Social Service] TEMPLATE[Template Service] LANDING[Landing Page Service] end subgraph Campaign Engine VALIDATOR[Content Validator] PERSONALIZER[Token Personalizer] SEGMENTER[Audience Segmenter] SCHEDULER[Send Scheduler] SMTP[SMTP Delivery Layer] WORKER1[Send Worker Pool] end subgraph Data Layer PG[(PostgreSQL
Core Data)] REDIS[(Redis
Cache + Sessions)] ES[(Elasticsearch
Search + Analytics)] S3[(Object Storage
Images + Assets)] end subgraph Async Processing KAFKA[Apache Kafka
Event Stream] DLQ[Dead Letter Queue] end subgraph Analytics CLICK[Click Processor] OPEN[Open Processor] BOUNCE[Bounce Processor] REPORT[Report Generator] end WEB --> GW MOB --> GW API THIRD --> GW GW --> CAMPAIGN GW --> CONTACT GW --> EVENT GW --> SURVEY GW --> SOCIAL GW --> LANDING CAMPAIGN --> VALIDATOR VALIDATOR --> PERSONALIZER PERSONALIZER --> SEGMENTER SEGMENTER --> SCHEDULER SCHEDULER --> WORKER1 WORKER1 --> SMTP CAMPAIGN --> KAFKA KAFKA --> CLICK KAFKA --> OPEN KAFKA --> BOUNCE CLICK --> REPORT OPEN --> REPORT BOUNCE --> REPORT CAMPAIGN --> PG CONTACT --> PG EVENT --> PG SURVEY --> PG CAMPAIGN --> REDIS CONTACT --> REDIS TEMPLATE --> S3 LANDING --> CDN SMTP --> DLQ

Service Communication Patterns

Services communicate through two primary patterns. Synchronous gRPC calls handle requests requiring immediate responses, such as fetching a contact's details during campaign personalization or validating an event's capacity before allowing a registration. Asynchronous Kafka messages handle fire-and-forget operations like recording email engagement events, triggering downstream analytics processing, or sending notification webhooks to connected third-party systems.

Data Flow for Campaign Sending

sequenceDiagram participant User participant API participant CampaignSvc participant ContactSvc participant Queue participant Workers participant SMTP participant Analytics User->>API: POST /campaigns/{id}/send API->>CampaignSvc: Validate and prepare campaign CampaignSvc->>ContactSvc: Fetch target contacts ContactSvc-->>CampaignSvc: Contact list with segments CampaignSvc->>CampaignSvc: Personalize email content CampaignSvc->>Queue: Enqueue send tasks (batched) CampaignSvc-->>API: 202 Accepted (job_id) API-->>User: Campaign scheduled for delivery Queue->>Workers: Consume send tasks Workers->>SMTP: Deliver email via SMTP SMTP-->>Workers: Delivery status (sent/bounced/failed) Workers->>Analytics: Record delivery event Analytics->>Analytics: Update campaign metrics
Design Principle: Every external API call that triggers a long-running operation returns immediately with a job identifier. Clients poll a status endpoint or subscribe to webhooks for completion notifications. This keeps API latencies low and prevents thread exhaustion during large campaign sends.

7. Email Campaign Builder

The campaign builder is the centerpiece of the user experience. Small business owners typically have no HTML expertise, so the builder must provide an intuitive drag-and-drop interface that produces professional, mobile-responsive emails. The builder consists of three major subsystems: the visual editor, the template engine, and the content rendering pipeline.

Visual Editor Architecture

The drag-and-drop editor is a client-side React application that manages a block-based content model. Each email is composed of content blocks — text blocks, image blocks, button blocks, divider blocks, social link blocks, and spacer blocks. Users drag blocks from a sidebar palette into a canvas area, reorder them via drag-and-drop, and configure properties through a context-sensitive settings panel. The editor maintains a JSON representation of the email layout that is serialized and sent to the server for storage.

Every block type implements a standardized interface that defines its default properties, editable settings, and rendered output. Text blocks support rich formatting including bold, italic, links, and merge tags. Image blocks integrate with the stock image library and support cropping, alignment, and link wrapping. Button blocks offer customizable colors, border radius, padding, and click-through URLs. The editor auto-saves every 30 seconds to prevent data loss.

Template System

The template system provides over 200 pre-built templates organized by use case category: newsletters, promotions, holiday greetings, event invitations, welcome series, and re-engagement campaigns. Each template is defined as a JSON document describing its block structure, color scheme, font choices, and responsive breakpoints. Templates are rendered into HTML using a server-side Razor engine that applies the user's customizations on top of the base template structure.

Templates must produce HTML that renders correctly across the fragmented email client landscape. Gmail strips certain CSS properties, Outlook uses a Word-based rendering engine, and Apple Mail supports modern CSS features that other clients ignore. The rendering pipeline applies a series of transformations: inlining CSS properties that need to be inlined for compatibility, adding MSO conditional comments for Outlook, replacing modern CSS Grid layouts with table-based fallbacks, and minifying the output to reduce email size.

Content Rendering Pipeline

graph LR A[JSON Block Layout] --> B[Template Resolver] B --> C[Razor Engine] C --> D[CSS Inliner] D --> E[Outlook Fallback] E --> F[Image Processing] F --> G[HTML Minification] G --> H[Size Validation] H --> I[Final HTML Output]

The image processing step rewrites all image URLs to point to the CDN and applies format optimization. Source images uploaded by users are stored in original format in object storage, while the rendering pipeline generates WebP and compressed JPEG variants at multiple resolutions. When the email is opened, the recipient's email client receives appropriately sized and formatted images based on the client's capabilities.

Mobile-Responsive Design

Over 60% of marketing emails are opened on mobile devices. The template system enforces a mobile-first design philosophy where every template includes responsive CSS that restructures multi-column layouts into single-column stacks on screens narrower than 480px. The builder includes a mobile preview mode that simulates rendering on iPhone and Android devices, showing users exactly how their email will appear before sending.

Stock Image Library

The platform integrates with licensed stock image providers to offer thousands of free-to-use images that small businesses can insert directly into their campaigns. The image library API supports keyword search, category browsing, and recent usage tracking. Images are served through the CDN with automatic format conversion and size optimization. Users can also upload their own images, which are stored in object storage and processed through the same optimization pipeline.

Block TypePropertiesMobile Behavior
TextContent, font, size, color, alignment, paddingFull width, adjusted font size
ImageSrc, alt, width, alignment, link URL, cropFull width, responsive scaling
ButtonLabel, URL, color, text color, border radius, paddingFull width, larger tap target
SocialIcon set, alignment, platform URLsWrapped to center
DividerColor, thickness, width, paddingFull width
SpacerHeightReduced height
Columns (2)Column ratio, content per columnStacked single column
Columns (3)Column ratio, content per columnStacked single column
Performance Optimization: The editor loads assets lazily — the base editor JavaScript is approximately 180KB gzipped, with block type modules loaded on demand. The stock image library uses infinite scroll with thumbnail previews, loading full-resolution images only when the user selects one.

8. Event Marketing System

The event marketing system enables small businesses to create, promote, and manage events directly from the Constant Contact platform. This goes far beyond simple calendar invitations — it includes full event page creation, RSVP tracking with multiple attendance states, waitlist management, automated email reminders, and on-site check-in functionality. The system is designed to handle everything from a local bakery's cooking class with 20 attendees to a chamber of commerce networking event with 2,000 guests.

Event Page Creation

Each event gets a dedicated landing page hosted on the platform. The event page builder uses a simplified version of the campaign builder's drag-and-drop editor, with event-specific content blocks for date/time display, location maps, speaker bios, agenda timelines, and registration forms. The page is fully responsive and branded with the account's logo and color scheme. Custom domain mapping allows businesses to host events under their own brand URL.

The event creation form captures essential details: title, description, date and time with timezone selection, location (physical address or virtual meeting link), capacity limits, ticket pricing (free or paid), and registration form fields. For recurring events, the system generates a series of occurrences from a recurrence rule, each with independent capacity and registration tracking.

RSVP and Registration Management

When a contact registers through the event page, the system creates a registration record in the confirmed state (if capacity is available) or the waitlisted state (if the event is at capacity). The registration captures the contact's information, any custom form responses, payment status for paid events, and a unique QR code for check-in. Registration confirmation emails are sent immediately and include the QR code, event details, and calendar download links (.ics format).

stateDiagram-v2 [*] --> Pending: Registration Created Pending --> Confirmed: Capacity Available Pending --> Waitlisted: Event Full Waitlisted --> Confirmed: Spot Opens Waitlisted --> Cancelled: Contact Cancels Confirmed --> CheckedIn: Check-in at Event Confirmed --> Cancelled: Contact Cancels Confirmed --> NoShow: Event Past + No Check-in Cancelled --> [*] NoShow --> [*] CheckedIn --> [*]

Waitlist and Automatic Promotion

The waitlist operates on a first-come-first-served basis. When a confirmed registrant cancels, the system automatically promotes the next person on the waitlist to the confirmed state, sends them a confirmation email, and notifies them of the change. If the event has a waitlist limit, registration pages display the waitlist position to give prospective attendees a sense of their likelihood of getting in.

Automated Reminder System

The event system includes a configurable reminder engine that sends email reminders at intervals before each event occurrence. Default reminders are sent 1 week, 1 day, and 1 hour before the event, but users can customize these intervals. Each reminder is generated from a template that includes the event details, the registrant's confirmation status, and a calendar attachment. Reminders are only sent to confirmed registrants — waitlisted and cancelled registrations are excluded.

The reminder scheduler is implemented as a background job that runs every 15 minutes. It queries for events starting within each reminder window, filters the registrant list by status, and enqueues individual reminder emails. The system respects the user's email frequency preferences and avoids sending duplicate reminders if the system experiences a processing delay.

Check-In System

At event time, organizers use a mobile-optimized check-in interface accessible from any browser. The check-in page supports three methods: scanning QR codes with the device camera, searching by name or email, and browsing the registration list with one-tap check-in buttons. Each check-in is recorded with a timestamp and the method used, providing accurate attendance data for post-event analytics.

FeatureImplementationData Source
Event PageServer-rendered with client-side hydrationEvent table + template cache
RegistrationTransactional with capacity lockRegistration table with row-level locking
WaitlistFIFO queue with automatic promotionWaitlist position column on registrations
RemindersBackground scheduler, 15-minute intervalsEvent start time + registration status
Check-inMobile PWA with offline supportRegistration table + check-in events
PaymentsStripe integration for paid eventsPayment records linked to registrations

9. Survey and Feedback Builder

The survey and feedback builder allows small businesses to create professional surveys for customer feedback, event evaluation, market research, and satisfaction measurement. The builder must be accessible to non-technical users while supporting sophisticated features like logic branching, response validation, and real-time analytics. Surveys can be distributed via email campaigns, shared links, website embeds, or QR codes.

Question Types

The platform supports ten distinct question types, each with specific configuration options and response formats. Multiple choice questions allow single or multi-select behavior with optional "other" fields. Rating questions use a configurable scale from 1-5 or 1-10, displayed as stars, numbers, or emoji faces. Net Promoter Score (NPS) questions use the standard 0-10 scale with automatic categorization into promoters, passives, and detractors. Open text questions support single-line, multi-line, and email-validated input modes. Dropdown questions handle long option lists efficiently with search-as-you-type filtering.

Logic Branching Engine

Logic branching enables surveys to adapt dynamically based on respondent answers. The branching engine evaluates conditions after each question and determines the next question to display. Conditions can reference any previous answer using operators like equals, not equals, greater than, contains, and range checks. Multiple conditions can be combined with AND/OR logic. The branching model is stored as a directed acyclic graph (DAG) where each node is a question and each edge is a conditional transition.

graph TD Q1[Q1: How did you hear about us?] -->|Social Media| Q2A[Q2a: Which platform?] Q1 -->|Friend Referral| Q2B[Q2b: What is their name?] Q1 -->|Search Engine| Q2C[Q2c: What did you search?] Q2A --> Q3[Q3: How would you rate your experience?] Q2B --> Q3 Q2C --> Q3 Q3 -->|Rating >= 4| Q4A[Q4a: Would you recommend us?] Q3 -->|Rating < 4| Q4B[Q4b: How can we improve?] Q4A --> Q5[Q5: Thank you message] Q4B --> Q5

Response Analytics

As responses flow in, the analytics engine computes real-time summary statistics. For multiple choice questions, response distributions are displayed as horizontal bar charts. Rating questions show average scores with standard deviation. NPS scores are calculated automatically with trend lines over time. Open text responses are displayed in a scrollable list with basic keyword highlighting. Cross-tabulation allows users to analyze how responses to one question correlate with responses to another.

The analytics dashboard supports filtering by date range, completion status, and response source (email, direct link, embed). Users can export raw response data as CSV or Excel files for further analysis in external tools. For surveys distributed via email campaigns, responses are linked to contact records, enabling segment-level analysis of survey results.

Survey Distribution Methods

  • Email Distribution: Embed a survey link in an email campaign with automatic contact association for logged-in users
  • Direct Link: Generate a shareable URL for distribution through any channel
  • Website Embed: Provide an iframe embed code and JavaScript snippet for inline website surveys
  • QR Code: Generate a QR code linking to the survey for print materials and in-person collection
  • Post-Event: Automatically send a satisfaction survey after an event check-in
Question TypeData FormatAnalyticsMax Options
Multiple Choice (Single)Option IDDistribution chart50
Multiple Choice (Multi)Option ID[]Distribution chart50
Rating (1-5)Integer 1-5Average, histogram
Rating (1-10)Integer 1-10Average, histogram
NPSInteger 0-10NPS score, categories
Open Text (Short)String (max 200)Word cloud, keyword
Open Text (Long)String (max 2000)Word cloud, keyword
DropdownOption IDDistribution chart200
DateISO 8601 dateRange, distribution
EmailEmail stringValidation stats
Privacy-First Design: Anonymous surveys strip all identifying information before storage. For authenticated surveys, the contact association is recorded but the response details can be anonymized in analytics exports if the account owner enables the privacy mode setting.

10. Social Media Integration

Social media integration extends the reach of email marketing campaigns beyond the inbox. Constant Contact allows users to connect their Facebook Pages, Instagram Business accounts, and Google Business Profiles to schedule and publish content directly from the platform. The integration layer handles OAuth authentication, content format adaptation, platform-specific rate limits, and engagement metrics aggregation.

OAuth Connection Flow

Each social platform requires a separate OAuth 2.0 authorization flow. When a user connects a social account, the platform redirects them to the provider's authorization page with the appropriate scopes. For Facebook, this includes pages_manage_posts and pages_read_engagement. For Instagram, the instagram_basic and instagram_content_publish scopes are required. Google Business uses the business.manage scope. Upon successful authorization, the platform stores encrypted access tokens and refresh tokens, along with the page or account metadata needed for publishing.

Token refresh is handled automatically by a background service that runs every 30 minutes. It identifies tokens expiring within the next hour, uses the refresh token to obtain new access tokens, and updates the database. If a refresh fails (indicating the user has revoked access), the connection is marked as disconnected and the user is notified through the dashboard and email.

Content Publishing Pipeline

graph LR A[Content Created] --> B{Source Type} B -->|Email Campaign| C[Extract Key Visuals] B -->|Original Post| D[Validate Content] C --> D D --> E[Format for Platform] E --> F[Apply Character Limits] F --> G[Attach Media] G --> H{Schedule or Publish} H -->|Immediate| I[Platform API Call] H -->|Scheduled| J[Scheduler Queue] J --> K[Platform API Call] K --> L[Record Post ID] L --> M[Track Engagement]

Content Format Adaptation

Each social platform has distinct content requirements. Facebook supports long-form text posts with up to 63,206 characters, but posts exceeding 500 characters are truncated in the feed. Instagram captions are limited to 2,200 characters with only the first 125 visible before a "more" link. Google Business posts have a 1,500-character limit and require a call-to-action button. The publishing pipeline automatically truncates or adapts content to fit each platform's constraints while preserving the core message and call-to-action.

Image dimensions also vary by platform. The pipeline generates platform-optimized image crops: 1200x630 for Facebook link shares, 1080x1080 for Instagram square posts, 1080x1350 for Instagram portrait posts, and 1200x900 for Google Business. Source images are stored at full resolution, and the pipeline generates the required variants on demand using the image processing service.

Cross-Posting from Email Campaigns

When an email campaign is sent, users can optionally enable cross-posting to their connected social accounts. The system extracts the campaign's primary image, headline, and preview text, then creates a social post linking back to the campaign's web archive version. This allows recipients who forward the email to their networks to amplify the campaign's reach through social sharing. Cross-posted content is created as a draft requiring user approval before publishing, ensuring brand consistency.

Rate Limit Management

Each social platform enforces API rate limits that must be respected to avoid temporary or permanent bans. The publishing pipeline implements a token bucket rate limiter for each connected account, tracking both per-minute and per-day limits. When a rate limit is approached, posts are queued and processed in FIFO order with appropriate delays. If a rate limit error is returned by the platform, the post is retried after the specified cooldown period with exponential backoff.

PlatformText LimitImage SizeRate LimitToken Refresh
Facebook Pages63,206 chars1200x630200 posts/day60 days
Instagram Business2,200 chars1080x108025 posts/day60 days
Google Business1,500 chars1200x900100 posts/weekNo refresh

11. Contact Management

Contacts are the lifeblood of any email marketing platform. Constant Contact's contact management system handles import from multiple sources, deduplication, lifecycle tracking, segmentation, scoring, and automatic hygiene. The system must scale to billions of contact records while supporting fast queries for segmentation and real-time personalization during campaign sends.

Import Pipeline

The contact import pipeline processes CSV, Excel, and vCard files uploaded by users, as well as API-initiated bulk imports. The pipeline performs a series of validation and transformation steps: column type detection, email format validation, phone number normalization, custom field mapping, duplicate detection against existing contacts, and compliance checks against suppression lists. Imports are processed asynchronously with progress tracking visible in the dashboard.

Duplicate detection uses a composite key of normalized email address and account ID. When a duplicate is found, the import engine applies the configured merge strategy: skip the new record, update the existing record with new values, or add the contact to a review queue for manual resolution. The merge operation is idempotent, allowing safe re-imports of the same file without creating duplicate records.

Contact Lifecycle and Engagement Scoring

Every contact on the platform progresses through a lifecycle: active, inactive, and suppressed. Active contacts have engaged with at least one email in the past 90 days. Inactive contacts have not engaged in 90-180 days. Suppressed contacts have hard-bounced, unsubscribed, or been marked as spam complainers. The lifecycle state is recalculated daily by a background job that processes engagement events.

Contact scoring assigns a numerical value (0-100) to each contact based on their engagement behavior. The scoring model weights recent activity more heavily than historical activity. Opening an email adds 2 points, clicking a link adds 5 points, registering for an event adds 3 points, and submitting a survey response adds 3 points. Inactivity decays the score by 1 point per day of no engagement. High-scoring contacts are prioritized for campaign targeting and represent the most engaged segment of an audience.

Segmentation Engine

The segmentation engine enables users to create dynamic contact segments based on arbitrary criteria. Segments are defined as filter rules that reference contact attributes, engagement metrics, list membership, and event/survey participation. The engine translates segment definitions into optimized SQL queries that are executed against the contact database with result caching. Segments can be saved and reused across campaigns, with their membership refreshed in near real-time as new engagement events arrive.

Segment CriterionOperatorExample
Email engagementOpened/Clicked in last N daysOpened in last 30 days
List membershipIs in / Is not in listIs in "VIP Customers" list
Contact attributeEquals/Contains/Greater thanCity equals "Boston"
Event historyRegistered/Attended/MissedAttended any event in 2026
Survey responseAnswered/Not answeredNPS score greater than 8
Score rangeBetween/Less than/Greater thanScore greater than 70
Subscription dateBefore/After/BetweenSubscribed after Jan 1, 2026

Automatic List Hygiene

The system automatically maintains list health through several mechanisms. Hard bounces immediately suppress the email address. Soft bounces are tracked and suppressed after three consecutive failures. Unsubscribes are processed within minutes and the contact is removed from all future sends. Spam complaints trigger immediate suppression and an investigation into the campaign content and targeting that led to the complaint. A monthly hygiene job archives contacts who have been inactive for over 180 days and have not opened any of the last 10 campaigns.

12. Campaign Automation

Campaign automation transforms Constant Contact from a batch email tool into a marketing automation platform. Automation capabilities include scheduled sends based on calendar rules, triggered campaigns that respond to contact behavior, and drip sequences that deliver a series of emails over time. The automation engine must handle complex scheduling logic, maintain state across multi-step sequences, and integrate with the broader event and survey systems.

Scheduled Campaigns

The simplest automation is a scheduled campaign that sends at a specific date and time. The scheduler uses a priority queue sorted by scheduled time, with a polling loop that checks for due campaigns every minute. When a campaign becomes due, the scheduler validates the campaign state, acquires a distributed lock to prevent duplicate sends, and enqueues the send tasks. Timezone-aware scheduling ensures that campaigns targeting contacts in multiple time zones are delivered at the optimal local time for each recipient.

Triggered Campaigns

Triggered campaigns fire in response to specific contact events. Supported triggers include: contact added to a list, contact score exceeds a threshold, contact opens or clicks a previous campaign, event registration completed, survey response submitted, or a custom webhook event. The trigger engine monitors an event stream (powered by Kafka) and evaluates trigger conditions against a rules engine. When a condition is met, the associated campaign is instantiated and sent to the triggering contact.

Drip Sequence Engine

Drip sequences (also called autoresponders or nurture sequences) deliver a predefined series of emails with configurable delays between each step. A typical welcome sequence might send an immediate welcome email, a follow-up after 3 days, a product tutorial after 7 days, and a special offer after 14 days. Each step in the sequence can have its own audience filter, so contacts who meet certain criteria can skip steps or be removed from the sequence entirely.

graph TD START([Contact Added to List]) --> S1[Email 1: Welcome
Delay: 0 days] S1 --> EVAL1{Opened Email 1?} EVAL1 -->|Yes| S2[Email 2: Product Tour
Delay: 3 days] EVAL1 -->|No| S2B[Email 2B: Alternate Welcome
Delay: 3 days] S2 --> EVAL2{Clicked any link?} S2B --> EVAL2 EVAL2 -->|Yes| S3[Email 3: Advanced Tips
Delay: 7 days] EVAL2 -->|No| S3B[Email 3B: Re-engagement
Delay: 7 days] S3 --> S4[Email 4: Special Offer
Delay: 14 days] S3B --> S4 S4 --> END([Sequence Complete])

The drip engine maintains per-contact state tracking which step each contact is on, when they entered the sequence, and when they completed (or exited) each step. This state is stored in a dedicated automation_state table partitioned by account and sequence. The engine processes sequences in batches, querying for contacts whose next step is due and enqueuing the corresponding campaign sends. State transitions are recorded atomically to prevent double-sends during processing failures.

Automation Analytics

Each automation includes a funnel visualization showing how contacts flow through the sequence. The funnel displays entry count at each step, open rates, click rates, exit reasons (completed, unsubscribed, bounced, removed by rule), and time-to-completion statistics. Users can compare performance across different sequence variants and identify drop-off points where contacts disengage.

Automation TypeTriggerState ManagementMax Steps
ScheduledCalendar date/timeSent/Failed status1
TriggeredContact eventEvent log + dedup1
Drip SequenceList entry or triggerPer-contact step tracking50
RecurringRepetition scheduleInstance trackingUnlimited
Concurrency Warning: The drip engine must handle race conditions where a contact receives a trigger event while already mid-sequence. Implement idempotent state transitions using compare-and-swap operations to prevent a contact from being processed twice in the same step.

13. Landing Page Builder

Landing pages extend the platform's capabilities beyond the inbox. Small businesses use landing pages for lead capture, event registration, product promotion, and survey distribution. The landing page builder provides a simplified version of the email campaign editor, optimized for web rendering rather than email client compatibility. Pages are hosted on the platform's domain with optional custom domain mapping.

Template Library and Editor

The landing page template library includes templates optimized for specific conversion goals: email signup forms, event registration pages, product showcases, contact forms, and thank-you pages. Each template is fully customizable through the drag-and-drop editor, which supports additional block types not available in the email editor, including video embeds, countdown timers, testimonial carousels, and payment collection forms.

Landing pages are rendered as static HTML with client-side JavaScript for interactive elements. The build process generates optimized HTML, CSS, and JavaScript bundles that are deployed to the CDN. Form submissions are handled by a serverless function that validates inputs, stores submissions in the database, and triggers any associated automations (such as adding the submitter to a contact list or sending a confirmation email).

Conversion Tracking

Every landing page includes built-in conversion tracking that measures visitor behavior and form completion rates. The tracking system records page views, scroll depth, time on page, form field interactions, and successful submissions. This data feeds into the analytics dashboard alongside email campaign and event metrics, providing a unified view of marketing performance across all channels.

A/B Testing

Landing pages support A/B testing for headlines, images, form layouts, and call-to-action buttons. The testing framework splits incoming traffic between variants using a deterministic hash of the visitor's session ID, ensuring consistent experience within a single visit. Statistical significance is calculated using a Bayesian approach, and the system automatically routes 100% of traffic to the winning variant once the confidence interval exceeds 95%.

Block TypeCapabilitiesAnalytics
Hero ImageBackground image, overlay text, CTA buttonView rate, scroll past rate
FormCustom fields, validation, submit actionsStart rate, completion rate
CountdownEnd date/time, timezone, action on expiryEngagement time
VideoYouTube/Vimeo embed, autoplay optionPlay rate, watch duration
TestimonialsCarousel, quote, author, star ratingSwipe rate
PaymentStripe checkout, amount, descriptionInitiated vs completed

14. Reporting and Analytics

Analytics transform raw engagement data into actionable insights for small business owners. The reporting system must process billions of email events per month, compute aggregate metrics in near real-time, and present them through intuitive dashboards that non-technical users can understand. The analytics architecture separates real-time event processing from historical aggregation to balance freshness with computational efficiency.

Event Processing Pipeline

Email engagement events — opens, clicks, bounces, unsubscribes, and spam complaints — flow through a real-time event processing pipeline powered by Apache Kafka. Each event includes the campaign ID, contact ID, timestamp, and event-specific metadata (such as the clicked URL for click events or the bounce type for bounce events). The pipeline processes events in parallel across multiple consumer groups, each responsible for a different downstream task: updating per-campaign metrics, updating per-contact engagement history, triggering real-time alerts for deliverability issues, and feeding the analytics aggregation layer.

Metrics Calculation

The primary email metrics follow industry-standard definitions. Open rate is calculated as unique opens divided by delivered emails (excluding bounces). Click rate is calculated as unique clicks divided by delivered emails. Click-to-open rate is unique clicks divided by unique opens. Bounce rate is total bounces (hard and soft) divided by total sends. Unsubscribe rate is unique unsubscribes divided by delivered emails. These metrics are computed at both the individual campaign level and the account aggregate level.

graph TB subgraph Event Sources EMAIL[Email Events
Opens, Clicks, Bounces] EVENT[Event Data
Registrations, Check-ins] SURVEY[Survey Data
Responses, Completions] SOCIAL[Social Data
Likes, Shares, Comments] end subgraph Processing Layer KAFKA[Kafka Streams
Real-time Processing] BATCH[Spark Batch
Daily Aggregation] end subgraph Storage Layer TS[(TimescaleDB
Time-series Metrics)] ES2[(Elasticsearch
Searchable Reports)] CACHE2[(Redis
Dashboard Cache)] end subgraph Presentation Layer DASH[Dashboard
Real-time Widgets] EMAIL_RPT[Email Reports
Campaign Performance] EVENT_RPT[Event Reports
Registration Analytics] EXPORT[Export Service
CSV, PDF Reports] end EMAIL --> KAFKA EVENT --> KAFKA SURVEY --> KAFKA SOCIAL --> KAFKA KAFKA --> TS KAFKA --> CACHE2 BATCH --> ES2 TS --> DASH ES2 --> EMAIL_RPT CACHE2 --> DASH TS --> EXPORT ES2 --> EXPORT

Dashboard Widgets

The main analytics dashboard presents a summary view of account-wide performance with drill-down capabilities into individual campaigns, events, and surveys. Key widgets include: overall engagement trend (opens and clicks over time), top-performing campaigns ranked by click rate, contact growth chart showing net new contacts per week, deliverability health score combining bounce rate, complaint rate, and inbox placement rate, and a real-time activity feed showing recent engagement events as they occur.

Event and Survey Analytics

Event analytics track the full lifecycle from creation to post-event analysis. Metrics include total registrations, attendance rate (checked in divided by confirmed), registration-to-show ratio over time, average ticket revenue for paid events, and referral source breakdown showing which campaigns or channels drove the most registrations. Survey analytics provide response rate tracking, completion funnel visualization showing where respondents drop off, and cross-tabulation matrices for multi-question analysis.

Social Engagement Metrics

Social analytics aggregate engagement data from connected platforms. The dashboard shows per-platform metrics including post reach, engagement rate (likes plus comments plus shares divided by reach), follower growth attributed to campaign cross-posts, and best-performing content types. These metrics are refreshed hourly since social platform APIs impose rate limits on metrics queries.

Metric CategoryKey MetricsRefresh FrequencyRetention
Email EngagementOpen rate, click rate, bounce rate, unsubscribe rateReal-time (5 min)24 months
Contact GrowthNew contacts, churn, net growthDaily24 months
Event PerformanceRegistrations, attendance rate, revenueReal-time24 months
Survey AnalyticsResponse rate, completion rate, NPSReal-time24 months
Social EngagementReach, engagement rate, follower growthHourly12 months
DeliverabilityInbox rate, spam rate, blacklist statusReal-time24 months

15. Deliverability Management

Deliverability is the make-or-break capability of any email marketing platform. If emails do not reach the inbox, the platform has failed its core mission. Constant Contact invests heavily in deliverability infrastructure, reputation monitoring, and user education to maintain inbox placement rates above 98% across all sending IPs and domains.

Email Authentication

The platform implements three layers of email authentication to verify sender legitimacy. SPF (Sender Policy Framework) records authorize the platform's sending IPs on behalf of the customer's domain. DKIM (DomainKeys Identified Mail) cryptographically signs each email with a 2048-bit RSA key, allowing receiving servers to verify that the email was not tampered with in transit. DMARC (Domain-based Message Authentication, Reporting, and Conformance) ties SPF and DKIM together with a policy that tells receiving servers what to do with emails that fail authentication.

For customers who use their own sending domain (white-label sending), the platform provides a DNS setup wizard that generates the required SPF, DKIM, and DMARC records. The wizard validates DNS configuration before allowing the first campaign send, ensuring that authentication is properly configured from the start. For customers using the platform's shared sending domain, authentication is managed centrally.

IP Reputation Management

The sending infrastructure uses dedicated IP pools organized by customer tier and sending volume. High-volume senders get dedicated IPs that they alone control, while low-volume senders share IPs within reputation-matched pools. New IPs go through a warm-up period where sending volume is gradually increased over 2-4 weeks while monitoring delivery metrics. The reputation monitoring system checks blacklists (Spamhaus, Barracuda, Invaluable) every 15 minutes and alerts the deliverability team when any sending IP appears on a blacklist.

Content Filtering

Before a campaign is sent, the content validation engine analyzes the email for characteristics that commonly trigger spam filters. The engine checks for spammy phrases (act now, limited time, click here), excessive use of capital letters and exclamation marks, image-to-text ratio (emails that are mostly images trigger filters), link reputation (URLs known to be associated with spam), and attachment risk. Users receive a deliverability score with specific recommendations for improvement before they can send.

graph LR A[Campaign Created] --> B[Content Analysis] B --> C{Score > 80?} C -->|Yes| D[Approved for Send] C -->|No| E[Recommendations] E --> F[User Edits Content] F --> B D --> G[Authentication Check] G --> H{SPF + DKIM Valid?} H -->|Yes| I[IP Selection] H -->|No| J[DNS Setup Required] I --> K[Reputation Check] K --> L{IP Not Blacklisted?} L -->|Yes| M[Enqueue for Sending] L -->|No| N[IP Rotation / Warm-up] N --> M

Compliance and Regulatory Requirements

The platform enforces compliance with CAN-SPAM (US), CASL (Canada), GDPR (EU), and CCPA (California) regulations. Key compliance features include mandatory unsubscribe links in every email, physical mailing address in the email footer, one-click unsubscribe processing within 10 business days, consent tracking for GDPR-subscribed contacts, data export and deletion capabilities for GDPR data subject requests, and automatic suppression of contacts in regions where consent has not been obtained.

Compliance AreaRequirementImplementation
CAN-SPAMUnsubscribe link, physical addressAuto-injected in every email footer
CASLExpress consent trackingConsent timestamp stored per contact
GDPRRight to erasure, data portabilitySelf-serve export and deletion tools
CCPADo-not-sell flagContact preference attribute
AuthenticationSPF, DKIM, DMARCAutomated DNS setup wizard

16. Integration Ecosystem

A marketing platform's value multiplies when it connects with the tools small businesses already use. Constant Contact provides pre-built integrations with popular e-commerce, CRM, content management, and advertising platforms. Each integration is implemented as an adapter that translates between the platform's API and the external system's API, handling authentication, data mapping, sync scheduling, and error recovery.

Shopify Integration

The Shopify integration synchronizes customer and order data from Shopify stores into Constant Contact contact lists. When a customer places a purchase on Shopify, a webhook triggers the integration to create or update the contact record with purchase history attributes. Product catalog data can be pulled into email templates as dynamic content blocks, allowing merchants to showcase recently purchased or recommended products. Abandoned cart events trigger automated re-engagement campaigns.

WordPress Integration

The WordPress plugin adds Constant Contact signup forms to WordPress sites and blogs. Forms can be embedded in posts, pages, or widget areas using shortcodes or Gutenberg blocks. The plugin supports double opt-in for GDPR compliance and automatically syncs new subscribers to designated contact lists. For WooCommerce stores, the integration mirrors the Shopify functionality with purchase-based segmentation and product recommendations.

Salesforce Integration

The Salesforce CRM integration bidirectionally syncs contacts between Constant Contact and Salesforce. Contact updates in either system are propagated to the other using a conflict resolution strategy that favors the most recently updated record. Campaign engagement data from Constant Contact (opens, clicks, unsubscribes) is written back to Salesforce as campaign member statuses, giving sales teams visibility into marketing engagement during lead qualification.

Google Ads Integration

The Google Ads integration enables contact list-based audience targeting for remarketing campaigns. High-engagement segments from Constant Contact can be pushed to Google Ads as custom audiences, allowing businesses to target their email subscribers with display and search ads. Conversely, Google Ads conversion data can be imported to attribute ad-driven signups and purchases back to specific campaigns.

graph TB CC[Constant Contact
Core Platform] SHOPIFY[Shopify
E-commerce] WP[WordPress
CMS] SF[Salesforce
CRM] GA[Google Ads
Advertising] ZAP[Zapier
Automation] FB2[Facebook
Social] STRIPE[Stripe
Payments] SHOPIFY -->|Customer + Order Sync| CC CC -->|Product Recommendations| SHOPIFY WP -->|Form Submissions| CC CC -->|Engagement Data| SF SF -->|Contact Updates| CC CC -->|Custom Audiences| GA GA -->|Conversion Data| CC ZAP -->|Generic Webhooks| CC CC -->|Post Publishing| FB2 CC -->|Payment Processing| STRIPE

Webhook and Zapier Integration

For integrations not covered by pre-built connectors, the platform provides a comprehensive webhook system and Zapier integration. Webhooks fire for all major events: campaign sent, email opened, link clicked, contact created, event registered, survey submitted, and social post published. Each webhook delivery includes a HMAC-SHA256 signature for payload verification and is retried up to 5 times with exponential backoff on failure. The Zapier integration provides a no-code alternative, enabling connections to thousands of apps through Zapier's ecosystem.

IntegrationSync DirectionTriggerData Synced
ShopifyInbound (Shopify to CC)Webhook on order/customer eventsCustomers, orders, products
WordPressInbound (WP to CC)Form submissionSubscribers, preferences
SalesforceBidirectionalScheduled sync + real-time triggersContacts, campaign members
Google AdsOutbound (CC to GA)Audience segment updateCustom audiences
ZapierBidirectionalEvent-based triggersAll entity types via webhooks
StripeOutbound (CC to Stripe)Event ticket purchasePayment intents, charges

17. Database Design

The database layer is the foundation of data integrity for the entire platform. Given the scale of 600,000 accounts with billions of contacts and tens of billions of email events, the database architecture must handle both high-throughput writes (engagement events) and complex analytical queries (campaign reports) without either workload degrading the other. We employ a polyglot persistence strategy using PostgreSQL for transactional data, TimescaleDB for time-series metrics, and Elasticsearch for search and aggregation queries.

Campaign Metrics Schema

SQL
CREATE TABLE campaign_metrics (
    metric_id       BIGSERIAL PRIMARY KEY,
    campaign_id     UUID NOT NULL,
    contact_id      UUID NOT NULL,
    event_type      VARCHAR(20) NOT NULL,
    event_timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    metadata        JSONB,
    UNIQUE(campaign_id, contact_id, event_type)
);

CREATE INDEX idx_campaign_metrics_campaign
    ON campaign_metrics(campaign_id, event_type);

CREATE INDEX idx_campaign_metrics_contact
    ON campaign_metrics(contact_id, event_timestamp DESC);

SELECT create_hypertable('campaign_metrics', 'event_timestamp');

Event Registration Schema

SQL
CREATE TABLE event_registrations (
    registration_id   UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    event_id          UUID NOT NULL REFERENCES events(event_id),
    occurrence_id     UUID NOT NULL,
    contact_id        UUID REFERENCES contacts(contact_id),
    email_address     VARCHAR(255) NOT NULL,
    status            VARCHAR(20) NOT NULL DEFAULT 'confirmed',
    waitlist_position INTEGER,
    registered_at     TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    checked_in_at     TIMESTAMPTZ,
    checkin_method    VARCHAR(20),
    qr_code_token     VARCHAR(64) UNIQUE NOT NULL,
    custom_fields     JSONB DEFAULT '{}',
    payment_status    VARCHAR(20),
    payment_amount    DECIMAL(10,2),
    payment_ref       VARCHAR(100)
);

CREATE INDEX idx_registrations_event
    ON event_registrations(event_id, status);

CREATE INDEX idx_registrations_contact
    ON event_registrations(contact_id, registered_at DESC);

CREATE INDEX idx_registrations_qr
    ON event_registrations(qr_code_token);

Survey Response Schema

SQL
CREATE TABLE survey_responses (
    response_id    UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    survey_id      UUID NOT NULL REFERENCES surveys(survey_id),
    contact_id     UUID REFERENCES contacts(contact_id),
    is_anonymous   BOOLEAN NOT NULL DEFAULT FALSE,
    started_at     TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    completed_at   TIMESTAMPTZ,
    completion_pct INTEGER DEFAULT 0,
    source         VARCHAR(20) NOT NULL,
    ip_address     INET,
    user_agent     TEXT
);

CREATE TABLE survey_answers (
    answer_id     UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    response_id   UUID NOT NULL REFERENCES survey_responses(response_id),
    question_id   UUID NOT NULL REFERENCES survey_questions(question_id),
    answer_value  JSONB NOT NULL,
    answered_at   TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_answers_response
    ON survey_answers(response_id);

CREATE INDEX idx_answers_question
    ON survey_answers(question_id, answer_value);

Database Sharding Strategy

All core tables are sharded by account_id using application-level routing. A consistent hash ring maps each account_id to one of N database shards. When a request arrives, the account_id from the authentication token determines the target shard, ensuring all data for a single account is co-located for efficient joins and transactions. The sharding key is included in every table's primary key or unique index to enable shard-local query routing without scatter-gather operations.

Cross-shard queries are limited to the analytics aggregation layer, which maintains pre-computed rollups in a separate analytical database. These rollups are updated incrementally as engagement events are processed and provide the data foundation for the reporting dashboard without impacting transactional query performance.

TableShard KeyGrowth RateRetention
contactsaccount_id50M rows/monthIndefinite (archived after 2yr inactivity)
campaign_metricsaccount_id + campaign_id5B rows/month24 months raw, indefinite aggregates
event_registrationsaccount_id + event_id5M rows/monthIndefinite
survey_responsesaccount_id + survey_id30M rows/month24 months raw, indefinite aggregates
email_eventsaccount_id + campaign_id10B rows/month6 months raw, 24 months aggregates

18. Caching Strategy

Caching is critical for reducing database load and improving dashboard responsiveness. The platform employs a multi-tier caching strategy using Redis for hot data, CDN edge caching for static assets, and application-level in-memory caching for frequently accessed reference data. Each cache tier has specific invalidation strategies to ensure data consistency.

Template Cache

Email and landing page templates are cached at three levels. The CDN caches rendered template HTML with a 5-minute TTL for published templates that are actively being sent. Redis caches the template JSON definition with a 15-minute TTL, reducing the database read load during campaign builder sessions. The application server caches the Razor compilation output in memory for the lifetime of the process, avoiding repeated template compilation for high-frequency templates.

Contact Cache

Contact data used during campaign personalization is cached in Redis with a 5-minute TTL. The cache key is derived from the contact_id, and the cached value includes all custom fields needed for personalization tokens. During a campaign send, the personalizer checks the cache before querying the database, reducing database reads by approximately 85% for repeat contacts across campaigns. Cache invalidation occurs when the contact record is updated through the API or import pipeline.

Analytics Cache

Campaign metrics displayed on the dashboard are cached in Redis with a 60-second TTL for real-time campaigns (sent within the last 24 hours) and a 5-minute TTL for historical campaigns. The cache stores pre-computed aggregate metrics (total opens, unique opens, total clicks, unique clicks, bounce count) rather than raw event data, minimizing cache size. Real-time metrics use a cache-aside pattern with write-through on event processing to maintain freshness.

Session and Rate Limit Cache

User session data is stored in Redis with a 24-hour sliding expiration. API rate limit counters use Redis sorted sets with atomic increment operations, enabling accurate rate limiting without external dependencies. The rate limit window is implemented using a sliding window algorithm that provides smoother throttling compared to fixed-window approaches.

Cache LayerTechnologyTTLInvalidationHit Rate Target
CDN EdgeCloudFront / Fastly5 min - 24 hrsPurge on deploy> 95%
Application MemoryIMemoryCacheProcess lifetimeOn config change> 99%
Distributed CacheRedis Cluster60s - 15 minWrite-through + TTL> 85%
Database Query CachePostgreSQL shared_buffersOS managedAutomatic LRU> 90%
Cache Stampede Prevention: When a popular campaign's metrics expire from cache simultaneously during high dashboard traffic, the system uses probabilistic early expiration (a random 10% of requests refresh the cache before TTL expires) combined with distributed locks to prevent multiple concurrent database queries for the same cache key.

19. Multi-Region Design

As Constant Contact serves businesses globally, a multi-region deployment strategy ensures low latency for international users and compliance with data residency regulations. The platform deploys primary regions in North America (us-east-1), Europe (eu-west-1), and Asia-Pacific (ap-southeast-1), with read replicas and CDN edge nodes distributed globally.

Data Residency and Sovereignty

GDPR requires that EU customer data be processed within the European Economic Area. The platform enforces data residency at the account level: when an EU business signs up, their account and all associated data are provisioned in the eu-west-1 region. Contact data, campaign content, event registrations, and survey responses never leave the region unless the account owner explicitly opts into global features (such as social media integration, which requires API calls to US-based social platforms). Similar residency controls apply to other regulated markets.

Cross-Region Architecture

graph TB subgraph NA[North America - us-east-1] API_NA[API Gateway] PG_NA[(PostgreSQL Primary)] REDIS_NA[(Redis Cluster)] KAFKA_NA[Kafka Cluster] SMTP_NA[SMTP Pool - NA IPs] end subgraph EU[Europe - eu-west-1] API_EU[API Gateway] PG_EU[(PostgreSQL Primary)] PG_EU_RO[(Read Replica)] REDIS_EU[(Redis Cluster)] KAFKA_EU[Kafka Cluster] SMTP_EU[SMTP Pool - EU IPs] end subgraph AP[Asia Pacific - ap-southeast-1] API_AP[API Gateway] PG_AP[(PostgreSQL Primary)] PG_AP_RO[(Read Replica)] REDIS_AP[(Redis Cluster)] SMTP_AP[SMTP Pool - AP IPs] end subgraph Global[Global Infrastructure] CDN_G[CDN Edge Nodes] DNS_G[Global DNS / GeoDNS] end DNS_G --> API_NA DNS_G --> API_EU DNS_G --> API_AP CDN_G --> API_NA CDN_G --> API_EU CDN_G --> API_AP PG_NA -.->|Async Replication| PG_EU_RO PG_NA -.->|Async Replication| PG_AP_RO

Region-Aware Send Routing

Email sending is routed to the region where the recipient's contact record is stored, ensuring compliance with data residency requirements. However, SMTP delivery IPs are selected based on the recipient's geographic location to optimize deliverability. A contact stored in the EU region whose email address is hosted by a European ISP is routed through European sending IPs, even if the campaign was created by a user in North America. This geo-aware routing is managed by a global send coordination service that maps recipient regions to optimal sending infrastructure.

Disaster Recovery

Each region maintains a hot standby in a separate availability zone. In the event of a primary database failure, the standby is promoted within 60 seconds. The RPO (Recovery Point Objective) is 5 seconds for the primary region due to synchronous replication within the availability zone, and 30 seconds for cross-region replicas using asynchronous replication. The RTO (Recovery Time Objective) is 2 minutes for automated failover and 15 minutes for manual failover scenarios.

RegionPrimary UseSMTP IPsCompliance
us-east-1North America accounts50 dedicated IPsCAN-SPAM, CCPA
eu-west-1EU accounts30 dedicated IPsGDPR, ePrivacy
ap-southeast-1APAC accounts20 dedicated IPsLocal regulations
us-west-2DR standby for us-east-120 dedicated IPsCAN-SPAM

20. Cost Estimation

Operating a platform at the scale of Constant Contact involves significant infrastructure costs. Below we estimate monthly costs for the major infrastructure components based on the capacity estimates established earlier in this guide. All costs are approximate and based on US East region pricing for major cloud providers.

ComponentSpecificationMonthly Cost (USD)
Application Servers64x c6i.2xlarge (8 vCPU, 16GB),000
PostgreSQL (Primary + Replicas)16x r6i.4xlarge, 10TB gp3 storage,000
Redis Cluster24x r6g.xlarge nodes, 500GB total,000
Apache Kafka12x kafka.m5.2xlarge brokers,000
Elasticsearch8x r6i.xlarge.search nodes, 4TB,000
Object Storage (S3)100TB (images, assets, exports),500
CDN (CloudFront)50TB/month transfer, 100M requests,000
SMTP Infrastructure120 dedicated IPs across 3 regions,000
DNS (Route 53)600K hosted zones + queries,500
Monitoring (Datadog)100 hosts, APM, logs, metrics,000
Load Balancers6 ALBs across regions,000
NAT Gateways6 NAT gateways across regions,000
Staff (Engineering Team)40 engineers + 5 SREs,200,000
Stock Image LicensingEnterprise license (50K+ images),000
Email Validation Service1.5B contacts validated,000
Third-Party APIsSocial, payment, CRM integrations,000

Cost Optimization Strategies

  • Reserved Instances: 70% of compute capacity is reserved for 1-year terms, providing 30-40% savings over on-demand pricing. This reduces the compute line items by approximately ,000/month.
  • Spot Instances for Batch Processing: Analytics aggregation, import processing, and report generation run on spot instances with 60-70% discount, saving approximately ,000/month.
  • Tiered Storage: Email event data older than 6 months is migrated to cold storage (S3 Glacier Deep Archive), reducing storage costs by 80% for historical data.
  • Connection Pooling: PgBouncer manages PostgreSQL connection pooling, reducing the number of database connections required and allowing fewer database instances to handle the same workload.
  • Batch Writes: Engagement events are batched in Kafka and written to the database in bulk inserts of 10,000 rows, reducing write amplification by 50x compared to individual inserts.
Total Estimated Monthly Cost: Approximately .45 million/month (.4M annually) including infrastructure and personnel. The infrastructure-only cost is approximately ,000/month, which represents roughly .42 per account per month — well within the economics of a -50/month subscription price point.

21. Interview Questions and Answers

The following questions are commonly asked in system design interviews for email marketing platform roles. Each answer demonstrates the depth of understanding expected at the senior and staff engineer level.

Q1: How would you design the email sending pipeline to handle 30,000 emails per second during peak periods?

The pipeline uses a three-tier queuing architecture. The first tier is an in-memory buffer on the campaign service that batches individual send requests into groups of 500. The second tier is a Kafka topic partitioned by target domain (gmail.com, outlook.com, yahoo.com, etc.) to enable per-domain rate limiting. The third tier is a Redis-backed priority queue that orders sends by campaign priority and recipient engagement score, ensuring that high-value recipients receive emails first. Worker pools consume from the domain-specific Kafka topics and maintain persistent SMTP connections to each ISP's MX servers, with connection pools sized to match each ISP's throughput limits. Backpressure is applied upstream when any domain's queue depth exceeds a threshold, temporarily pausing sends to that domain and allowing the queue to drain.

Q2: How do you prevent duplicate emails when a campaign send fails mid-way through processing?

Idempotency is enforced at three levels. Each send task carries a unique idempotency key derived from the campaign_id and contact_id. The worker process checks this key against a Redis set before attempting delivery, and marks it as in-progress atomically using a SETNX operation. If the worker crashes after SMTP handoff but before completing the task, the in-progress key expires after 5 minutes and the task is retried. The SMTP layer also assigns a unique Message-ID header that the platform tracks, so any duplicate delivery attempt detects the existing Message-ID and skips re-sending. The combination of Redis-based deduplication and SMTP-level Message-ID tracking prevents virtually all duplicate sends.

Q3: How would you design the contact segmentation engine to support complex queries over billions of contacts?

Segment definitions are compiled into optimized SQL queries that leverage composite indexes on the contact table's common filter columns (engagement_score, last_opened_at, list_membership, custom fields). The most frequently used segments are materialized as database views that are refreshed every 5 minutes. For ad-hoc segments with complex nested conditions, the query engine generates a query plan that evaluates the cheapest filters first (indexed columns) before applying expensive operations (regex matching on custom fields). Results are cached in Redis with a 60-second TTL. For segments used in campaign sends, the membership is snapshotted at send time to ensure consistency throughout the send process.

Q4: How do you handle the case where a user imports a CSV with 10 million contacts?

The import pipeline processes large files asynchronously in configurable batch sizes. The CSV is streamed from the upload endpoint to object storage, then processed by a fleet of import workers. Each worker reads a batch of 10,000 rows, performs validation, deduplication against existing contacts (using a Bloom filter for fast negative checks followed by exact database lookups for potential matches), and writes valid records in bulk inserts. Progress is tracked in a Redis counter and exposed through a status API that the frontend polls. The user receives email notification when the import completes, along with a summary of records imported, duplicates detected, and validation errors. The entire 10M contact import completes in approximately 45 minutes with 20 parallel workers.

Q5: How would you ensure high deliverability across different ISPs?

Deliverability management is a multi-layered system. First, the platform maintains separate IP pools for different reputation tiers, warming up new IPs gradually over 2-4 weeks. Second, sending volume per IP is throttled based on the IP's reputation score, which is calculated from real-time delivery metrics (bounce rate, complaint rate, inbox placement rate). Third, the content validation engine pre-screens campaigns for spam triggers before they enter the send pipeline. Fourth, feedback loops from ISPs (complaint notifications, bounce notifications) are processed in real-time to suppress problematic contacts immediately. Fifth, a deliverability monitoring team uses third-party seed list services to measure actual inbox placement rates across major ISPs daily. Sixth, per-domain sending rates are dynamically adjusted based on real-time delivery success rates, automatically slowing down if a particular ISP begins throttling or rejecting messages.

Q6: How do you handle the event registration race condition when two people register simultaneously for the last spot?

The registration endpoint acquires a distributed lock on the event_id using Redis with a 5-second TTL. Within the lock, the system checks current registration count against capacity using a SELECT FOR UPDATE query on the event row. If capacity is available, the registration is created and the count is incremented atomically. If capacity is full, the registration is created in waitlisted status. The lock is released after the transaction commits. This ensures that exactly one registration gets the last spot even under concurrent access. The PostgreSQL row-level lock provides a second layer of safety in case the Redis lock expires due to GC pauses or network issues. The event's capacity counter is also maintained as a denormalized column updated within the same transaction, avoiding count queries on every registration attempt.

Q7: How would you design the survey logic branching engine to handle complex conditional paths?

The survey structure is modeled as a directed acyclic graph where nodes are questions and edges are conditional transitions. Each edge contains a condition expression that evaluates the respondent's previous answers. When a respondent completes a question, the branching engine evaluates all outgoing edges from the current node, selects the first edge whose condition evaluates to true, and routes to the corresponding next question. If no edge matches, the survey proceeds to the next question in linear order (default fallback). The condition evaluator supports a rich expression language with operators for equality, comparison, string matching, and set membership. The entire branching graph is validated at survey publish time to detect unreachable questions (questions that no path can reach), infinite loops (cycles in the graph), and conflicting conditions (multiple edges from the same node that could both be true simultaneously).

Q8: How do you handle social media API rate limits when you have thousands of accounts posting simultaneously?

Each connected social account has a rate limiter implementation using a token bucket algorithm in Redis. The token bucket is configured with the platform's known rate limits (e.g., 25 posts/day for Instagram, 200 posts/day for Facebook). When a post is enqueued, the system checks if tokens are available in the bucket. If tokens are available, the post proceeds and tokens are consumed. If not, the post is scheduled for the next available window, which is calculated based on the token refill rate. The rate limit state is shared across all application instances using Redis, ensuring consistent rate limiting even when posts are processed by different workers. If a platform returns a 429 (rate limit exceeded) response, the system backfills the token bucket to match the Retry-After header value and pauses sends to that account for the specified duration.

Q9: How would you design the analytics pipeline to provide real-time campaign metrics while also supporting historical analysis?

The analytics pipeline uses a lambda architecture with both real-time and batch processing layers. The real-time layer processes Kafka events through Apache Flink, computing running aggregates (total opens, unique opens, click counts) in 5-minute tumbling windows and storing results in Redis and TimescaleDB. The batch layer runs daily Spark jobs that recompute all metrics from the raw event data, providing accurate historical numbers that correct any missed events from the real-time layer. The dashboard reads from Redis for the current campaign's real-time metrics and from TimescaleDB for historical trends. Elasticsearch stores denormalized campaign records with embedded metrics for fast search and filtering. The reconciliation process between real-time and batch layers runs every 24 hours and flags any discrepancies greater than 0.1% for investigation.

Q10: How would you handle GDPR data deletion requests across all platform subsystems?

A GDPR deletion request triggers a coordinated purge across all data stores. The deletion service first identifies all data associated with the contact: campaign_metrics, event_registrations, survey_responses, contact attributes, engagement history, and any cached data. Deletions are processed in a specific order: first, the contact is added to a suppression list to prevent future data creation. Then, soft-deletes are applied to transactional tables (contacts, registrations) to maintain referential integrity. Hard-deletes are applied to engagement history and analytics data. Cache entries are explicitly evicted from Redis. CDN-cached email opens and clicks are invalidated. A deletion audit log is maintained (without personal data) to prove compliance. The entire process completes within 72 hours, well within the 30-day GDPR requirement. A verification job runs weekly to confirm that no personal data has leaked through backup systems or downstream replicas.

Q11: How do you design the platform to support white-label resellers who want to offer email marketing under their own brand?

White-label support requires multi-tenant isolation at the branding, domain, and infrastructure levels. Each reseller account has a configuration record storing their custom logo URL, color scheme, domain mappings, and SMTP sending domain. The application reads the reseller configuration from a cache at request time and applies branding overrides to all UI rendering and email templates. Custom domain mapping is implemented at the CDN level, routing subdomains like mail.reseller.com to the platform's web application with the reseller's branding context. SMTP sending uses dedicated IP pools per reseller to isolate reputation, and custom DKIM keys are generated per reseller domain. API responses include reseller metadata that white-label frontends can use to customize their own client-facing interfaces.

Q12: How would you monitor and respond to a sudden drop in deliverability for a specific ISP?

The monitoring system computes a real-time inbox placement score for each ISP by analyzing bounce codes, complaint rates, and seed list test results. If the score drops below a configured threshold (e.g., inbox rate falls from 98% to 92% for Gmail), an automated alert fires to the deliverability team via PagerDuty. Simultaneously, the system reduces sending volume to the affected ISP by 50% as an automatic containment measure. The deliverability engineer investigates root causes: content changes across recent campaigns, a spike in hard bounces indicating list quality issues, or blacklist appearances. If the issue is content-related, the affected campaigns are paused and users are notified. If it is an IP reputation issue, the system rotates to backup IPs for the affected ISP domain. Resolution metrics (time to detect, time to contain, time to resolve) are tracked and reported in weekly deliverability reviews.

22. Full C# Implementation

The following C# implementation demonstrates the core service layer for the Constant Contact-like platform. The code covers the CampaignService for email campaign lifecycle management, the EventManager for event registration and check-in, the SurveyBuilder for survey creation and response collection, and the SocialScheduler for cross-platform content publishing. The implementation uses ASP.NET Core, Entity Framework Core, and the StackExchange.Redis library.

Campaign Service — Campaign Creation and Send Orchestration

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using StackExchange.Redis;
using Newtonsoft.Json;

namespace ConstantContact.Platform.Services
{
    public enum CampaignStatus
    {
        Draft, Validating, Scheduled, Sending,
        Sent, Paused, Failed, Cancelled
    }

    public class Campaign
    {
        public Guid Id { get; set; }
        public Guid AccountId { get; set; }
        public string Name { get; set; }
        public string Subject { get; set; }
        public string PreviewText { get; set; }
        public string HtmlBody { get; set; }
        public string PlainTextBody { get; set; }
        public CampaignStatus Status { get; set; }
        public List<TargetList> TargetLists { get; set; }
        public SendConfiguration SendConfig { get; set; }
        public ABTestConfig ABTest { get; set; }
        public DateTime CreatedAt { get; set; }
        public DateTime? ScheduledAt { get; set; }
        public DateTime? SentAt { get; set; }
        public CampaignMetrics Metrics { get; set; }
    }

    public class SendConfiguration
    {
        public string Mode { get; set; }
        public DateTime? ScheduledAt { get; set; }
        public string TimezoneBehavior { get; set; }
        public BatchConfig BatchConfig { get; set; }
    }

    public class BatchConfig
    {
        public int BatchSize { get; set; } = 50000;
        public int BatchIntervalMinutes { get; set; } = 15;
        public int ThrottlePerMinute { get; set; } = 5000;
    }

    public class ABTestConfig
    {
        public bool Enabled { get; set; }
        public int VariantCount { get; set; } = 2;
        public string WinnerMetric { get; set; } = "open_rate";
        public int SamplePercentage { get; set; } = 20;
        public int WinnerSendDelayHours { get; set; } = 4;
    }

    public class CampaignMetrics
    {
        public int TotalSent { get; set; }
        public int TotalDelivered { get; set; }
        public int UniqueOpens { get; set; }
        public int TotalOpens { get; set; }
        public int UniqueClicks { get; set; }
        public int TotalClicks { get; set; }
        public int HardBounces { get; set; }
        public int SoftBounces { get; set; }
        public int Unsubscribes { get; set; }
        public int SpamComplaints { get; set; }

        public double OpenRate =>
            TotalDelivered > 0
            ? (double)UniqueOpens / TotalDelivered * 100 : 0;
        public double ClickRate =>
            TotalDelivered > 0
            ? (double)UniqueClicks / TotalDelivered * 100 : 0;
        public double BounceRate =>
            TotalSent > 0
            ? (double)(HardBounces + SoftBounces) / TotalSent * 100 : 0;
    }

    public interface ICampaignService
    {
        Task<Campaign> CreateCampaignAsync(
            Guid accountId, Campaign campaign);
        Task<Campaign> SendCampaignAsync(
            Guid accountId, Guid campaignId);
        Task<CampaignMetrics> GetMetricsAsync(
            Guid accountId, Guid campaignId);
        Task<ValidationResult> ValidateContentAsync(
            Campaign campaign);
    }

    public class CampaignService : ICampaignService
    {
        private readonly AppDbContext _db;
        private readonly IConnectionMultiplexer _redis;
        private readonly IEmailValidator _validator;
        private readonly IPersonalizationEngine _personalizer;
        private readonly ITemplateRenderer _renderer;
        private readonly ISendQueue _sendQueue;
        private readonly ILogger<CampaignService> _logger;

        public CampaignService(
            AppDbContext db,
            IConnectionMultiplexer redis,
            IEmailValidator validator,
            IPersonalizationEngine personalizer,
            ITemplateRenderer renderer,
            ISendQueue sendQueue,
            ILogger<CampaignService> logger)
        {
            _db = db;
            _redis = redis;
            _validator = validator;
            _personalizer = personalizer;
            _renderer = renderer;
            _sendQueue = sendQueue;
            _logger = logger;
        }

        public async Task<Campaign> CreateCampaignAsync(
            Guid accountId, Campaign campaign)
        {
            campaign.Id = Guid.NewGuid();
            campaign.AccountId = accountId;
            campaign.Status = CampaignStatus.Draft;
            campaign.CreatedAt = DateTime.UtcNow;
            campaign.Metrics = new CampaignMetrics();

            _db.Campaigns.Add(campaign);
            await _db.SaveChangesAsync();

            _logger.LogInformation(
                "Campaign {Id} created for account {AccountId}",
                campaign.Id, accountId);

            return campaign;
        }

        public async Task<ValidationResult> ValidateContentAsync(
            Campaign campaign)
        {
            var result = new ValidationResult();

            if (string.IsNullOrWhiteSpace(campaign.Subject))
                result.Errors.Add("Subject line is required");

            if (campaign.Subject?.Length > 150)
                result.Errors.Add(
                    "Subject line exceeds 150 characters");

            if (string.IsNullOrWhiteSpace(campaign.HtmlBody))
                result.Errors.Add("Email body is required");

            if (campaign.HtmlBody?.Length > 102400)
                result.Errors.Add(
                    "Email body exceeds 100KB limit");

            var spamScore = await _validator
                .CalculateSpamScoreAsync(campaign.HtmlBody);
            result.SpamScore = spamScore;

            if (spamScore > 20)
                result.Warnings.Add(
                    "High spam score detected. " +
                    "Review content for spam triggers.");

            var links = _validator
                .ExtractLinks(campaign.HtmlBody ?? "");
            foreach (var link in links)
            {
                var reputation = await _validator
                    .CheckLinkReputationAsync(link);
                if (reputation == LinkReputation.Malicious)
                    result.Errors.Add(
                        $"Malicious link detected: {link}");
            }

            result.IsValid = result.Errors.Count == 0;
            return result;
        }

        public async Task<Campaign> SendCampaignAsync(
            Guid accountId, Guid campaignId)
        {
            var campaign = await _db.Campaigns
                .Include(c => c.TargetLists)
                .FirstOrDefaultAsync(c =>
                    c.Id == campaignId &&
                    c.AccountId == accountId);

            if (campaign == null)
                throw new NotFoundException(
                    "Campaign not found");

            if (campaign.Status != CampaignStatus.Draft)
                throw new InvalidOperationException(
                    $"Campaign cannot be sent " +
                    $"from status {campaign.Status}");

            var validation = await ValidateContentAsync(campaign);
            if (!validation.IsValid)
                throw new ValidationException(
                    validation.Errors);

            campaign.Status = CampaignStatus.Validating;
            await _db.SaveChangesAsync();

            var recipients = await GetRecipientsAsync(campaign);

            if (campaign.SendConfig?.Mode == "scheduled"
                && campaign.SendConfig.ScheduledAt.HasValue)
            {
                campaign.Status = CampaignStatus.Scheduled;
                campaign.ScheduledAt =
                    campaign.SendConfig.ScheduledAt.Value;

                await _sendQueue.ScheduleCampaignAsync(
                    campaign, recipients);

                _logger.LogInformation(
                    "Campaign {Id} scheduled for {Time}",
                    campaign.Id, campaign.ScheduledAt);
            }
            else
            {
                campaign.Status = CampaignStatus.Sending;
                campaign.SentAt = DateTime.UtcNow;
                await _db.SaveChangesAsync();

                await _sendQueue.EnqueueCampaignAsync(
                    campaign, recipients);

                _logger.LogInformation(
                    "Campaign {Id} sending to {Count} " +
                    "recipients",
                    campaign.Id, recipients.Count);
            }

            await _db.SaveChangesAsync();
            return campaign;
        }

        public async Task<CampaignMetrics> GetMetricsAsync(
            Guid accountId, Guid campaignId)
        {
            var cacheKey =
                $"campaign:metrics:{campaignId}";
            var cache = _redis.GetDatabase();
            var cached = await cache
                .StringGetAsync(cacheKey);

            if (cached.HasValue)
                return JsonConvert
                    .DeserializeObject<CampaignMetrics>(
                        cached);

            var campaign = await _db.Campaigns
                .FirstOrDefaultAsync(c =>
                    c.Id == campaignId &&
                    c.AccountId == accountId);

            if (campaign == null)
                throw new NotFoundException(
                    "Campaign not found");

            await cache.StringSetAsync(
                cacheKey,
                JsonConvert.SerializeObject(
                    campaign.Metrics),
                TimeSpan.FromSeconds(60));

            return campaign.Metrics;
        }

        private async Task<List<ContactRecipient>>
            GetRecipientsAsync(Campaign campaign)
        {
            var contactIds = await _db.ContactListMembers
                .Where(m =>
                    campaign.TargetLists
                        .Any(tl => tl.ListId == m.ListId))
                .Select(m => m.ContactId)
                .Distinct()
                .ToListAsync();

            return await _db.Contacts
                .Where(c => contactIds.Contains(c.Id)
                    && c.Status == ContactStatus.Active)
                .Select(c => new ContactRecipient
                {
                    ContactId = c.Id,
                    Email = c.EmailAddress,
                    FirstName = c.FirstName,
                    LastName = c.LastName,
                    CustomFields = c.CustomFields
                })
                .ToListAsync();
        }
    }
}

Event Manager — Registration and Check-In

C#
using System;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;

namespace ConstantContact.Platform.Services
{
    public enum RegistrationStatus
    {
        Pending, Confirmed, Waitlisted,
        Cancelled, CheckedIn, NoShow
    }

    public class EventRegistration
    {
        public Guid Id { get; set; }
        public Guid EventId { get; set; }
        public Guid OccurrenceId { get; set; }
        public Guid? ContactId { get; set; }
        public string EmailAddress { get; set; }
        public RegistrationStatus Status { get; set; }
        public int? WaitlistPosition { get; set; }
        public string QrCodeToken { get; set; }
        public DateTime RegisteredAt { get; set; }
        public DateTime? CheckedInAt { get; set; }
        public string CheckinMethod { get; set; }
        public string PaymentStatus { get; set; }
        public decimal? PaymentAmount { get; set; }
        public Dictionary<string, string> CustomFields
            { get; set; }

        public static EventRegistration Promoted { get; set; }
    }

    public interface IEventManager
    {
        Task<EventRegistration> RegisterAsync(
            Guid eventId, Guid? contactId,
            string email,
            Dictionary<string, string> customFields);
        Task<EventRegistration> CheckInAsync(
            Guid eventId, string qrToken);
        Task<EventRegistration> CheckInByNameAsync(
            Guid eventId, string searchTerm);
        Task<int> GetWaitlistPositionAsync(
            Guid eventId, Guid registrationId);
        Task<CancelResult> CancelRegistrationAsync(
            Guid eventId, Guid registrationId);
    }

    public class EventManager : IEventManager
    {
        private readonly AppDbContext _db;
        private readonly IEmailService _emailService;
        private readonly IRedisLock _lockService;
        private readonly ILogger<EventManager> _logger;

        public EventManager(
            AppDbContext db,
            IEmailService emailService,
            IRedisLock lockService,
            ILogger<EventManager> logger)
        {
            _db = db;
            _emailService = emailService;
            _lockService = lockService;
            _logger = logger;
        }

        public async Task<EventRegistration> RegisterAsync(
            Guid eventId, Guid? contactId,
            string email,
            Dictionary<string, string> customFields)
        {
            var lockKey = $"event:lock:{eventId}";
            using var lockHandle = await _lockService
                .AcquireAsync(lockKey,
                    TimeSpan.FromSeconds(5));

            if (lockHandle == null)
                throw new ConcurrencyException(
                    "Event is currently processing " +
                    "registrations. Please retry.");

            var eventEntity = await _db.Events
                .Include(e => e.Occurrences)
                .FirstOrDefaultAsync(e => e.Id == eventId);

            if (eventEntity == null)
                throw new NotFoundException(
                    "Event not found");

            var occurrence = eventEntity.Occurrences
                .FirstOrDefault(o =>
                    o.StartDate > DateTime.UtcNow);

            if (occurrence == null)
                throw new InvalidOperationException(
                    "No upcoming occurrences available");

            var existingReg = await _db.EventRegistrations
                .FirstOrDefaultAsync(r =>
                    r.EventId == eventId
                    && r.OccurrenceId == occurrence.Id
                    && (r.EmailAddress == email
                        || (contactId.HasValue
                            && r.ContactId == contactId))
                    && r.Status != RegistrationStatus.Cancelled);

            if (existingReg != null)
                throw new InvalidOperationException(
                    "Already registered for this event");

            var confirmedCount = await _db
                .EventRegistrations
                .CountAsync(r =>
                    r.EventId == eventId
                    && r.OccurrenceId == occurrence.Id
                    && r.Status
                        == RegistrationStatus.Confirmed);

            var registration = new EventRegistration
            {
                Id = Guid.NewGuid(),
                EventId = eventId,
                OccurrenceId = occurrence.Id,
                ContactId = contactId,
                EmailAddress = email,
                QrCodeToken = GenerateQrToken(),
                RegisteredAt = DateTime.UtcNow,
                CustomFields = customFields ?? new()
            };

            if (confirmedCount < eventEntity.Capacity)
            {
                registration.Status =
                    RegistrationStatus.Confirmed;
            }
            else
            {
                registration.Status =
                    RegistrationStatus.Waitlisted;
                registration.WaitlistPosition =
                    confirmedCount - eventEntity.Capacity + 1;
            }

            _db.EventRegistrations.Add(registration);
            await _db.SaveChangesAsync();

            await _emailService.SendAsync(
                email,
                "Registration Confirmation",
                BuildConfirmationTemplate(
                    eventEntity, registration));

            _logger.LogInformation(
                "Registration {Id} for event {EventId}: " +
                "{Status}",
                registration.Id, eventId,
                registration.Status);

            return registration;
        }

        public async Task<EventRegistration> CheckInAsync(
            Guid eventId, string qrToken)
        {
            var registration = await _db.EventRegistrations
                .FirstOrDefaultAsync(r =>
                    r.EventId == eventId
                    && r.QrCodeToken == qrToken
                    && r.Status
                        == RegistrationStatus.Confirmed);

            if (registration == null)
                throw new NotFoundException(
                    "Valid registration not found");

            registration.Status =
                RegistrationStatus.CheckedIn;
            registration.CheckedInAt = DateTime.UtcNow;
            registration.CheckinMethod = "qr_scan";

            await _db.SaveChangesAsync();

            _logger.LogInformation(
                "Check-in: Registration {Id} " +
                "at event {EventId}",
                registration.Id, eventId);

            return registration;
        }

        public async Task<EventRegistration>
            CheckInByNameAsync(
                Guid eventId, string searchTerm)
        {
            var registration = await _db.EventRegistrations
                .FirstOrDefaultAsync(r =>
                    r.EventId == eventId
                    && r.Status
                        == RegistrationStatus.Confirmed
                    && (r.EmailAddress.Contains(searchTerm)
                        || (r.Contact != null
                            && (r.Contact.FirstName
                                    .Contains(searchTerm)
                                || r.Contact.LastName
                                    .Contains(searchTerm)))));

            if (registration == null)
                throw new NotFoundException(
                    "No matching registration found");

            registration.Status =
                RegistrationStatus.CheckedIn;
            registration.CheckedInAt = DateTime.UtcNow;
            registration.CheckinMethod = "name_search";

            await _db.SaveChangesAsync();
            return registration;
        }

        public async Task<CancelResult>
            CancelRegistrationAsync(
                Guid eventId, Guid registrationId)
        {
            var lockKey = $"event:lock:{eventId}";
            using var lockHandle = await _lockService
                .AcquireAsync(lockKey,
                    TimeSpan.FromSeconds(5));

            var registration = await _db.EventRegistrations
                .FirstOrDefaultAsync(r =>
                    r.Id == registrationId
                    && r.EventId == eventId);

            if (registration == null)
                throw new NotFoundException(
                    "Registration not found");

            var wasConfirmed = registration.Status
                == RegistrationStatus.Confirmed;
            registration.Status =
                RegistrationStatus.Cancelled;
            await _db.SaveChangesAsync();

            var promotedRegistration =
                EventRegistration.Promoted;

            if (wasConfirmed)
            {
                var nextWaitlisted = await _db
                    .EventRegistrations
                    .Where(r =>
                        r.EventId == eventId
                        && r.OccurrenceId
                            == registration.OccurrenceId
                        && r.Status
                            == RegistrationStatus.Waitlisted)
                    .OrderBy(r => r.WaitlistPosition)
                    .FirstOrDefaultAsync();

                if (nextWaitlisted != null)
                {
                    nextWaitlisted.Status =
                        RegistrationStatus.Confirmed;
                    nextWaitlisted.WaitlistPosition = null;
                    await _db.SaveChangesAsync();

                    await _emailService.SendAsync(
                        nextWaitlisted.EmailAddress,
                        "You're In!",
                        "A spot opened up and you " +
                        "are now confirmed!");

                    promotedRegistration = nextWaitlisted;
                }
            }

            return new CancelResult
            {
                CancelledRegistration = registration,
                PromotedRegistration = promotedRegistration
            };
        }

        public async Task<int> GetWaitlistPositionAsync(
            Guid eventId, Guid registrationId)
        {
            var registration = await _db.EventRegistrations
                .FirstOrDefaultAsync(r =>
                    r.Id == registrationId);

            return registration?.WaitlistPosition ?? -1;
        }

        private string GenerateQrToken()
        {
            var bytes = RandomNumberGenerator
                .GetBytes(32);
            return Convert
                .ToBase64String(bytes)
                .Replace("+", "-")
                .Replace("/", "_")
                .Substring(0, 43);
        }

        private string BuildConfirmationTemplate(
            Event evt, EventRegistration reg)
        {
            return $@"
                <h2>You're registered!</h2>
                <p>Event: {evt.Title}</p>
                <p>Date: {evt.NextOccurrenceDate}</p>
                <p>Status: {reg.Status}</p>
                <p>QR Code: {reg.QrCodeToken}</p>";
        }
    }
}

Survey Builder — Survey Creation and Response Processing

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;

namespace ConstantContact.Platform.Services
{
    public enum QuestionType
    {
        MultipleChoiceSingle,
        MultipleChoiceMulti,
        Rating1To5,
        Rating1To10,
        NPS,
        OpenTextShort,
        OpenTextLong,
        Dropdown,
        Date,
        Email
    }

    public class SurveyQuestion
    {
        public Guid Id { get; set; }
        public Guid SurveyId { get; set; }
        public string Text { get; set; }
        public QuestionType Type { get; set; }
        public bool IsRequired { get; set; }
        public int SortOrder { get; set; }
        public List<QuestionChoice> Choices { get; set; }
        public List<LogicBranch> Branches { get; set; }
        public Dictionary<string, object> Config { get; set; }
    }

    public class QuestionChoice
    {
        public Guid Id { get; set; }
        public string Label { get; set; }
        public int SortOrder { get; set; }
    }

    public class LogicBranch
    {
        public Guid Id { get; set; }
        public string Condition { get; set; }
        public Guid TargetQuestionId { get; set; }
    }

    public class SurveyResponse
    {
        public Guid Id { get; set; }
        public Guid SurveyId { get; set; }
        public Guid? ContactId { get; set; }
        public bool IsAnonymous { get; set; }
        public DateTime StartedAt { get; set; }
        public DateTime? CompletedAt { get; set; }
        public int CompletionPct { get; set; }
        public string Source { get; set; }
        public List<SurveyAnswer> Answers { get; set; }
    }

    public class SurveyAnswer
    {
        public Guid Id { get; set; }
        public Guid ResponseId { get; set; }
        public Guid QuestionId { get; set; }
        public string AnswerValue { get; set; }
        public DateTime AnsweredAt { get; set; }
    }

    public class SurveyAnalytics
    {
        public int TotalResponses { get; set; }
        public int CompletedResponses { get; set; }
        public double CompletionRate { get; set; }
        public double AvgTimeToComplete { get; set; }
        public List<QuestionAnalytics> QuestionStats
            { get; set; }
    }

    public class QuestionAnalytics
    {
        public Guid QuestionId { get; set; }
        public string QuestionText { get; set; }
        public QuestionType Type { get; set; }
        public int ResponseCount { get; set; }
        public Dictionary<string, int> Distribution
            { get; set; }
        public double? AverageValue { get; set; }
        public double? NpsScore { get; set; }
    }

    public interface ISurveyBuilder
    {
        Task<Survey> CreateSurveyAsync(
            Guid accountId, Survey survey);
        Task<SurveyQuestion> AddQuestionAsync(
            Guid surveyId, SurveyQuestion question);
        Task<Survey> PublishSurveyAsync(Guid surveyId);
        Task<SurveyResponse> SubmitResponseAsync(
            Guid surveyId, SurveyResponse response);
        Task<SurveyAnalytics> GetAnalyticsAsync(
            Guid surveyId);
    }

    public class SurveyBuilder : ISurveyBuilder
    {
        private readonly AppDbContext _db;
        private readonly IBranchEvaluator _branchEvaluator;
        private readonly ILogger<SurveyBuilder> _logger;

        public SurveyBuilder(
            AppDbContext db,
            IBranchEvaluator branchEvaluator,
            ILogger<SurveyBuilder> logger)
        {
            _db = db;
            _branchEvaluator = branchEvaluator;
            _logger = logger;
        }

        public async Task<Survey> CreateSurveyAsync(
            Guid accountId, Survey survey)
        {
            survey.Id = Guid.NewGuid();
            survey.AccountId = accountId;
            survey.Status = SurveyStatus.Draft;
            survey.CreatedAt = DateTime.UtcNow;
            survey.Questions = new List<SurveyQuestion>();

            _db.Surveys.Add(survey);
            await _db.SaveChangesAsync();

            return survey;
        }

        public async Task<SurveyQuestion> AddQuestionAsync(
            Guid surveyId, SurveyQuestion question)
        {
            var survey = await _db.Surveys
                .Include(s => s.Questions)
                .FirstOrDefaultAsync(s => s.Id == surveyId);

            if (survey == null)
                throw new NotFoundException(
                    "Survey not found");

            if (survey.Status != SurveyStatus.Draft)
                throw new InvalidOperationException(
                    "Cannot modify a published survey");

            question.Id = Guid.NewGuid();
            question.SurveyId = surveyId;
            question.SortOrder = survey.Questions.Count;

            _db.SurveyQuestions.Add(question);
            await _db.SaveChangesAsync();

            return question;
        }

        public async Task<Survey> PublishSurveyAsync(
            Guid surveyId)
        {
            var survey = await _db.Surveys
                .Include(s => s.Questions
                    .OrderBy(q => q.SortOrder))
                .ThenInclude(q => q.Choices)
                .Include(s => s.Questions
                    .SelectMany(q => q.Branches))
                .FirstOrDefaultAsync(s => s.Id == surveyId);

            if (survey == null)
                throw new NotFoundException(
                    "Survey not found");

            if (survey.Questions.Count == 0)
                throw new InvalidOperationException(
                    "Survey must have at least " +
                    "one question");

            var validationErrors =
                ValidateBranchGraph(survey);

            if (validationErrors.Any())
                throw new InvalidOperationException(
                    $"Invalid branching: " +
                    string.Join("; ",
                        validationErrors));

            survey.Status = SurveyStatus.Published;
            survey.PublishedAt = DateTime.UtcNow;
            survey.ShareUrl =
                $"https://surveys.platform.com/s/{surveyId}";

            await _db.SaveChangesAsync();
            return survey;
        }

        public async Task<SurveyResponse> SubmitResponseAsync(
            Guid surveyId, SurveyResponse response)
        {
            var survey = await _db.Surveys
                .Include(s => s.Questions
                    .OrderBy(q => q.SortOrder))
                .FirstOrDefaultAsync(s => s.Id == surveyId);

            if (survey == null || survey.Status
                != SurveyStatus.Published)
                throw new NotFoundException(
                    "Published survey not found");

            response.Id = Guid.NewGuid();
            response.SurveyId = surveyId;
            response.StartedAt = DateTime.UtcNow;
            response.Answers = response.Answers
                .Select(a =>
                {
                    a.Id = Guid.NewGuid();
                    a.ResponseId = response.Id;
                    a.AnsweredAt = DateTime.UtcNow;
                    return a;
                }).ToList();

            if (response.IsAnonymous)
                response.ContactId = null;

            var totalRequired = survey.Questions
                .Count(q => q.IsRequired);
            var answeredRequired = response.Answers
                .Count(a =>
                    survey.Questions.Any(q =>
                        q.Id == a.QuestionId
                        && q.IsRequired
                        && !string.IsNullOrEmpty(
                            a.AnswerValue)));

            response.CompletionPct =
                totalRequired > 0
                ? (int)((double)answeredRequired
                    / totalRequired * 100) : 100;

            if (response.CompletionPct == 100)
                response.CompletedAt = DateTime.UtcNow;

            _db.SurveyResponses.Add(response);
            await _db.SaveChangesAsync();

            _logger.LogInformation(
                "Survey response {Id} for survey " +
                "{SurveyId}, completion {Pct}%",
                response.Id, surveyId,
                response.CompletionPct);

            return response;
        }

        public async Task<SurveyAnalytics> GetAnalyticsAsync(
            Guid surveyId)
        {
            var responses = await _db.SurveyResponses
                .Where(r => r.SurveyId == surveyId)
                .ToListAsync();

            var analytics = new SurveyAnalytics
            {
                TotalResponses = responses.Count,
                CompletedResponses = responses.Count(r =>
                    r.CompletedAt.HasValue),
                CompletionRate =
                    responses.Count > 0
                    ? (double)responses.Count(r =>
                        r.CompletedAt.HasValue)
                        / responses.Count * 100 : 0,
                AvgTimeToComplete =
                    responses.Where(r =>
                        r.CompletedAt.HasValue)
                    .Average(r =>
                        r.CompletedAt.Value
                            .Subtract(r.StartedAt)
                            .TotalMinutes),
                QuestionStats =
                    new List<QuestionAnalytics>()
            };

            return analytics;
        }

        private List<string> ValidateBranchGraph(
            Survey survey)
        {
            var errors = new List<string>();
            var questionIds = survey.Questions
                .Select(q => q.Id).ToHashSet();

            foreach (var question in survey.Questions)
            {
                if (question.Branches != null)
                {
                    foreach (var branch in question.Branches)
                    {
                        if (!questionIds.Contains(
                            branch.TargetQuestionId))
                        {
                            errors.Add(
                                $"Question {question.Id} " +
                                $"branches to non-existent " +
                                $"question " +
                                $"{branch.TargetQuestionId}");
                        }
                    }
                }
            }

            return errors;
        }
    }
}

Social Scheduler — Cross-Platform Publishing

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;

namespace ConstantContact.Platform.Services
{
    public enum SocialPlatform
    {
        Facebook, Instagram, GoogleBusiness
    }

    public enum SocialPostStatus
    {
        Draft, Scheduled, Publishing,
        Published, Failed, RateLimited
    }

    public class SocialPost
    {
        public Guid Id { get; set; }
        public Guid AccountId { get; set; }
        public Guid? CampaignId { get; set; }
        public SocialPlatform Platform { get; set; }
        public string Content { get; set; }
        public List<string> ImageUrls { get; set; }
        public string LinkUrl { get; set; }
        public SocialPostStatus Status { get; set; }
        public DateTime? ScheduledAt { get; set; }
        public DateTime? PublishedAt { get; set; }
        public string PlatformPostId { get; set; }
        public string ErrorMessage { get; set; }
        public int RetryCount { get; set; }
    }

    public class SocialConnection
    {
        public Guid Id { get; set; }
        public Guid AccountId { get; set; }
        public SocialPlatform Platform { get; set; }
        public string PageId { get; set; }
        public string PageName { get; set; }
        public string EncryptedAccessToken { get; set; }
        public string EncryptedRefreshToken { get; set; }
        public DateTime? TokenExpiresAt { get; set; }
        public bool IsActive { get; set; }
    }

    public interface ISocialScheduler
    {
        Task<SocialPost> CreatePostAsync(
            Guid accountId, SocialPost post);
        Task<SocialPost> PublishNowAsync(
            Guid accountId, Guid postId);
        Task<SocialPost> SchedulePostAsync(
            Guid accountId, Guid postId, DateTime when);
        Task<SocialPost> CrossPostFromCampaignAsync(
            Guid accountId, Guid campaignId,
            List<SocialPlatform> platforms);
        Task ProcessScheduledPostsAsync();
        Task RefreshTokensAsync();
    }

    public class SocialScheduler : ISocialScheduler
    {
        private readonly AppDbContext _db;
        private readonly ISocialMediaClient _socialClient;
        private readonly IRateLimiter _rateLimiter;
        private readonly IContentAdapter _contentAdapter;
        private readonly ILogger<SocialScheduler> _logger;

        private static readonly Dictionary<SocialPlatform,
            int> DailyLimits = new()
        {
            [SocialPlatform.Facebook] = 200,
            [SocialPlatform.Instagram] = 25,
            [SocialPlatform.GoogleBusiness] = 15
        };

        private static readonly Dictionary<SocialPlatform,
            int> CharLimits = new()
        {
            [SocialPlatform.Facebook] = 63206,
            [SocialPlatform.Instagram] = 2200,
            [SocialPlatform.GoogleBusiness] = 1500
        };

        public SocialScheduler(
            AppDbContext db,
            ISocialMediaClient socialClient,
            IRateLimiter rateLimiter,
            IContentAdapter contentAdapter,
            ILogger<SocialScheduler> logger)
        {
            _db = db;
            _socialClient = socialClient;
            _rateLimiter = rateLimiter;
            _contentAdapter = contentAdapter;
            _logger = logger;
        }

        public async Task<SocialPost> CreatePostAsync(
            Guid accountId, SocialPost post)
        {
            var connection = await _db.SocialConnections
                .FirstOrDefaultAsync(c =>
                    c.AccountId == accountId
                    && c.Platform == post.Platform
                    && c.IsActive);

            if (connection == null)
                throw new InvalidOperationException(
                    $"No active {post.Platform} " +
                    $"connection found");

            post.Id = Guid.NewGuid();
            post.AccountId = accountId;
            post.Status = SocialPostStatus.Draft;

            post.Content = _contentAdapter
                .TruncateForPlatform(
                    post.Content, post.Platform);

            _db.SocialPosts.Add(post);
            await _db.SaveChangesAsync();

            return post;
        }

        public async Task<SocialPost> PublishNowAsync(
            Guid accountId, Guid postId)
        {
            var post = await GetPostAsync(
                accountId, postId);
            var connection = await GetConnectionAsync(
                accountId, post.Platform);

            var hasTokens = await _rateLimiter
                .TryConsumeTokensAsync(
                    post.Platform, accountId, 1);

            if (!hasTokens)
            {
                post.Status =
                    SocialPostStatus.RateLimited;
                post.ErrorMessage =
                    "Rate limit reached. " +
                    "Scheduled for next available window.";
                await _db.SaveChangesAsync();
                return post;
            }

            post.Status = SocialPostStatus.Publishing;
            await _db.SaveChangesAsync();

            try
            {
                var result = await _socialClient
                    .PublishAsync(
                        post.Platform,
                        connection.EncryptedAccessToken,
                        connection.PageId,
                        post.Content,
                        post.ImageUrls,
                        post.LinkUrl);

                post.Status = SocialPostStatus.Published;
                post.PublishedAt = DateTime.UtcNow;
                post.PlatformPostId = result.PostId;
            }
            catch (Exception ex)
            {
                _logger.LogError(ex,
                    "Failed to publish post {Id} " +
                    "to {Platform}",
                    postId, post.Platform);

                post.Status = SocialPostStatus.Failed;
                post.ErrorMessage = ex.Message;
                post.RetryCount++;
            }

            await _db.SaveChangesAsync();
            return post;
        }

        public async Task<SocialPost> SchedulePostAsync(
            Guid accountId, Guid postId, DateTime when)
        {
            var post = await GetPostAsync(
                accountId, postId);

            post.ScheduledAt = when;
            post.Status = SocialPostStatus.Scheduled;
            await _db.SaveChangesAsync();

            return post;
        }

        public async Task<SocialPost>
            CrossPostFromCampaignAsync(
                Guid accountId, Guid campaignId,
                List<SocialPlatform> platforms)
        {
            var campaign = await _db.Campaigns
                .FirstOrDefaultAsync(c =>
                    c.Id == campaignId
                    && c.AccountId == accountId);

            if (campaign == null)
                throw new NotFoundException(
                    "Campaign not found");

            var content = _contentAdapter
                .ExtractSocialContent(campaign);
            var imageUrls = _contentAdapter
                .ExtractCampaignImages(campaign);

            var firstPost = new SocialPost();
            var results = new List<SocialPost>();

            foreach (var platform in platforms)
            {
                var post = new SocialPost
                {
                    AccountId = accountId,
                    CampaignId = campaignId,
                    Platform = platform,
                    Content = content,
                    ImageUrls = imageUrls,
                    LinkUrl =
                        $"https://archive.platform.com" +
                        $"/c/{campaignId}",
                    Status = SocialPostStatus.Draft
                };

                var created = await CreatePostAsync(
                    accountId, post);
                results.Add(created);
            }

            return results.First();
        }

        public async Task ProcessScheduledPostsAsync()
        {
            var now = DateTime.UtcNow;
            var duePosts = await _db.SocialPosts
                .Where(p =>
                    p.Status == SocialPostStatus.Scheduled
                    && p.ScheduledAt <= now)
                .ToListAsync();

            foreach (var post in duePosts)
            {
                await PublishNowAsync(
                    post.AccountId, post.Id);
            }
        }

        public async Task RefreshTokensAsync()
        {
            var expiringSoon = await _db.SocialConnections
                .Where(c =>
                    c.IsActive
                    && c.EncryptedRefreshToken != null
                    && c.TokenExpiresAt <
                        DateTime.UtcNow.AddHours(1))
                .ToListAsync();

            foreach (var conn in expiringSoon)
            {
                try
                {
                    var newToken = await _socialClient
                        .RefreshAccessTokenAsync(
                            conn.Platform,
                            conn.EncryptedRefreshToken);

                    conn.EncryptedAccessToken =
                        newToken.AccessToken;
                    conn.TokenExpiresAt =
                        newToken.ExpiresAt;

                    await _db.SaveChangesAsync();
                }
                catch (Exception ex)
                {
                    _logger.LogWarning(ex,
                        "Token refresh failed for " +
                        "connection {Id}, " +
                        "marking inactive",
                        conn.Id);

                    conn.IsActive = false;
                    await _db.SaveChangesAsync();
                }
            }
        }

        private async Task<SocialPost> GetPostAsync(
            Guid accountId, Guid postId)
        {
            var post = await _db.SocialPosts
                .FirstOrDefaultAsync(p =>
                    p.Id == postId
                    && p.AccountId == accountId);

            if (post == null)
                throw new NotFoundException(
                    "Social post not found");

            return post;
        }

        private async Task<SocialConnection>
            GetConnectionAsync(
                Guid accountId, SocialPlatform platform)
        {
            var conn = await _db.SocialConnections
                .FirstOrDefaultAsync(c =>
                    c.AccountId == accountId
                    && c.Platform == platform
                    && c.IsActive);

            if (conn == null)
                throw new InvalidOperationException(
                    $"No active {platform} connection");

            return conn;
        }
    }
}

23. Conclusion

Designing a Constant Contact-like platform is a masterclass in balancing competing concerns: simplicity for non-technical users against sophisticated backend capabilities, high throughput for email delivery against strict deliverability requirements, and multi-tenancy isolation against resource efficiency. The system we have designed in this guide addresses all of these tensions through a carefully layered architecture that separates user-facing concerns from data processing concerns, and data processing concerns from infrastructure concerns.

The email campaign engine handles the highest-volume workload through a multi-tier queuing architecture that provides per-domain rate limiting, idempotent delivery, and real-time metrics tracking. The event marketing system manages complex state transitions through distributed locking and atomic capacity management, while the survey builder supports sophisticated logic branching through a directed acyclic graph model. Social media integration respects platform-specific constraints through content adaptation and token bucket rate limiting.

At the data layer, the partition-by-account strategy ensures that multi-tenant isolation is maintained without sacrificing query performance. The polyglot persistence approach — PostgreSQL for transactions, TimescaleDB for time-series metrics, Elasticsearch for search, and Redis for caching — ensures that each workload is handled by the most appropriate storage technology. The caching strategy balances freshness against load reduction through tiered TTLs and cache stampede prevention mechanisms.

The multi-region deployment strategy addresses both performance and compliance requirements. By routing data storage to the appropriate region based on the customer's location and routing email delivery through region-optimized SMTP pools, the platform maintains low latency and regulatory compliance simultaneously. The disaster recovery design with hot standbys and asynchronous cross-region replication provides robust protection against regional failures.

For engineering teams building similar platforms, the key takeaways are: invest heavily in deliverability infrastructure as it directly impacts customer value, design the campaign sending pipeline for horizontal scalability from day one, implement idempotency at every level to prevent data corruption during failures, and maintain strict account-level data partitioning to support both performance and compliance requirements. The small business email marketing space continues to grow, and platforms that combine ease of use with enterprise-grade reliability will capture the most value in this market.

Summary of Key Design Decisions: Multi-tier queuing for email throughput, Kafka-based event streaming for analytics, Redis distributed locking for capacity management, DAG-based branching for surveys, token bucket rate limiting for social APIs, account-level sharding for multi-tenancy, and lambda architecture for real-time plus batch analytics. Every decision optimizes for the dual constraints of small business usability and enterprise-scale reliability.

© 2026 Ayodhyya. All rights reserved.

This article was published on July 1, 2026.