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
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.
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
| Requirement | Target | Rationale |
|---|---|---|
| Availability | 99.95% uptime | Small businesses depend on timely campaign delivery |
| Email Throughput | 10M emails/hour peak | Seasonal spikes during holidays and promotions |
| Latency (Campaign Send) | P99 < 2 seconds for API acknowledgment | Immediate feedback on send initiation |
| Latency (Dashboard Load) | P95 < 800ms | Responsive user experience for non-technical users |
| Data Durability | Zero data loss for contacts and campaigns | Regulatory and trust requirements |
| Scalability | Linear horizontal scaling to 2M+ accounts | Growth target over 3 years |
| Security | SOC 2 Type II compliant, GDPR ready | Enterprise customer requirements and regulatory compliance |
| Deliverability | > 98% inbox placement rate | Core 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.
| Metric | Average | Peak | Storage |
|---|---|---|---|
| Active Accounts | 600,000 | 650,000 | — |
| Total Contacts | 1.5 billion | 1.8 billion | ~10 TB |
| Emails per Month | 7.5 billion | 30 billion (holiday) | ~50 TB logs |
| Emails per Second | ~3,000 | ~30,000 | — |
| Event Registrations per Month | 4.5 million | 20 million | ~2 TB |
| Survey Responses per Month | 24 million | 80 million | ~5 TB/year |
| Campaign Reports Generated | 3 million/day | 15 million/day | ~15 TB/year |
| Social Posts per Month | 1.2 million | 5 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.
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
Key Table Definitions
| Table | Primary Key | Partition Key | Estimated Rows |
|---|---|---|---|
| accounts | account_id | account_id | 600,000 |
| contacts | contact_id | account_id | 1.5 billion |
| contact_lists | list_id | account_id | 5 million |
| contact_list_members | contact_id + list_id | account_id | 3 billion |
| campaigns | campaign_id | account_id | 50 million |
| campaign_recipients | id | campaign_id | 30 billion |
| events | event_id | account_id | 5 million |
| event_registrations | registration_id | event_id | 500 million |
| surveys | survey_id | account_id | 2 million |
| survey_responses | response_id | survey_id | 2 billion |
| email_events | event_id | campaign_id | 50 billion |
| social_posts | post_id | account_id | 10 million |
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
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/v1/campaigns | Create a new email campaign |
| GET | /api/v1/campaigns | List 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}/send | Schedule or immediately send the campaign |
| POST | /api/v1/campaigns/{id}/duplicate | Create a copy of an existing campaign |
| GET | /api/v1/campaigns/{id}/metrics | Get real-time performance metrics |
Contact Endpoints
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/v1/contacts | Create a single contact |
| POST | /api/v1/contacts/bulk | Import 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}/engagement | Get full engagement timeline for a contact |
| POST | /api/v1/contacts/segment | Query contacts by segment criteria |
Event Endpoints
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/v1/events | Create a new event with details and capacity |
| GET | /api/v1/events | List events with date range and status filters |
| PUT | /api/v1/events/{id} | Update event details |
| POST | /api/v1/events/{id}/register | Register a contact for an event |
| GET | /api/v1/events/{id}/registrations | List all registrations with status |
| POST | /api/v1/events/{id}/checkin | Check in a registrant at the event |
| GET | /api/v1/events/{id}/report | Get event performance report |
Survey Endpoints
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/v1/surveys | Create 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}/publish | Publish survey and generate shareable link |
| POST | /api/v1/surveys/{id}/responses | Submit a survey response |
| GET | /api/v1/surveys/{id}/analytics | Get response analytics and cross-tabulation |
| GET | /api/v1/surveys/{id}/export | Export 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/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.
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
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
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 Type | Properties | Mobile Behavior |
|---|---|---|
| Text | Content, font, size, color, alignment, padding | Full width, adjusted font size |
| Image | Src, alt, width, alignment, link URL, crop | Full width, responsive scaling |
| Button | Label, URL, color, text color, border radius, padding | Full width, larger tap target |
| Social | Icon set, alignment, platform URLs | Wrapped to center |
| Divider | Color, thickness, width, padding | Full width |
| Spacer | Height | Reduced height |
| Columns (2) | Column ratio, content per column | Stacked single column |
| Columns (3) | Column ratio, content per column | Stacked single column |
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).
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.
| Feature | Implementation | Data Source |
|---|---|---|
| Event Page | Server-rendered with client-side hydration | Event table + template cache |
| Registration | Transactional with capacity lock | Registration table with row-level locking |
| Waitlist | FIFO queue with automatic promotion | Waitlist position column on registrations |
| Reminders | Background scheduler, 15-minute intervals | Event start time + registration status |
| Check-in | Mobile PWA with offline support | Registration table + check-in events |
| Payments | Stripe integration for paid events | Payment 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.
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 Type | Data Format | Analytics | Max Options |
|---|---|---|---|
| Multiple Choice (Single) | Option ID | Distribution chart | 50 |
| Multiple Choice (Multi) | Option ID[] | Distribution chart | 50 |
| Rating (1-5) | Integer 1-5 | Average, histogram | — |
| Rating (1-10) | Integer 1-10 | Average, histogram | — |
| NPS | Integer 0-10 | NPS score, categories | — |
| Open Text (Short) | String (max 200) | Word cloud, keyword | — |
| Open Text (Long) | String (max 2000) | Word cloud, keyword | — |
| Dropdown | Option ID | Distribution chart | 200 |
| Date | ISO 8601 date | Range, distribution | — |
| Email string | Validation stats | — |
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 Criterion | Operator | Example |
|---|---|---|
| Email engagement | Opened/Clicked in last N days | Opened in last 30 days |
| List membership | Is in / Is not in list | Is in "VIP Customers" list |
| Contact attribute | Equals/Contains/Greater than | City equals "Boston" |
| Event history | Registered/Attended/Missed | Attended any event in 2026 |
| Survey response | Answered/Not answered | NPS score greater than 8 |
| Score range | Between/Less than/Greater than | Score greater than 70 |
| Subscription date | Before/After/Between | Subscribed 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.
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 Type | Trigger | State Management | Max Steps |
|---|---|---|---|
| Scheduled | Calendar date/time | Sent/Failed status | 1 |
| Triggered | Contact event | Event log + dedup | 1 |
| Drip Sequence | List entry or trigger | Per-contact step tracking | 50 |
| Recurring | Repetition schedule | Instance tracking | Unlimited |
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 Type | Capabilities | Analytics |
|---|---|---|
| Hero Image | Background image, overlay text, CTA button | View rate, scroll past rate |
| Form | Custom fields, validation, submit actions | Start rate, completion rate |
| Countdown | End date/time, timezone, action on expiry | Engagement time |
| Video | YouTube/Vimeo embed, autoplay option | Play rate, watch duration |
| Testimonials | Carousel, quote, author, star rating | Swipe rate |
| Payment | Stripe checkout, amount, description | Initiated 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.
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 Category | Key Metrics | Refresh Frequency | Retention |
|---|---|---|---|
| Email Engagement | Open rate, click rate, bounce rate, unsubscribe rate | Real-time (5 min) | 24 months |
| Contact Growth | New contacts, churn, net growth | Daily | 24 months |
| Event Performance | Registrations, attendance rate, revenue | Real-time | 24 months |
| Survey Analytics | Response rate, completion rate, NPS | Real-time | 24 months |
| Social Engagement | Reach, engagement rate, follower growth | Hourly | 12 months |
| Deliverability | Inbox rate, spam rate, blacklist status | Real-time | 24 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.
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 Area | Requirement | Implementation |
|---|---|---|
| CAN-SPAM | Unsubscribe link, physical address | Auto-injected in every email footer |
| CASL | Express consent tracking | Consent timestamp stored per contact |
| GDPR | Right to erasure, data portability | Self-serve export and deletion tools |
| CCPA | Do-not-sell flag | Contact preference attribute |
| Authentication | SPF, DKIM, DMARC | Automated 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.
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.
| Integration | Sync Direction | Trigger | Data Synced |
|---|---|---|---|
| Shopify | Inbound (Shopify to CC) | Webhook on order/customer events | Customers, orders, products |
| WordPress | Inbound (WP to CC) | Form submission | Subscribers, preferences |
| Salesforce | Bidirectional | Scheduled sync + real-time triggers | Contacts, campaign members |
| Google Ads | Outbound (CC to GA) | Audience segment update | Custom audiences |
| Zapier | Bidirectional | Event-based triggers | All entity types via webhooks |
| Stripe | Outbound (CC to Stripe) | Event ticket purchase | Payment 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.
| Table | Shard Key | Growth Rate | Retention |
|---|---|---|---|
| contacts | account_id | 50M rows/month | Indefinite (archived after 2yr inactivity) |
| campaign_metrics | account_id + campaign_id | 5B rows/month | 24 months raw, indefinite aggregates |
| event_registrations | account_id + event_id | 5M rows/month | Indefinite |
| survey_responses | account_id + survey_id | 30M rows/month | 24 months raw, indefinite aggregates |
| email_events | account_id + campaign_id | 10B rows/month | 6 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 Layer | Technology | TTL | Invalidation | Hit Rate Target |
|---|---|---|---|---|
| CDN Edge | CloudFront / Fastly | 5 min - 24 hrs | Purge on deploy | > 95% |
| Application Memory | IMemoryCache | Process lifetime | On config change | > 99% |
| Distributed Cache | Redis Cluster | 60s - 15 min | Write-through + TTL | > 85% |
| Database Query Cache | PostgreSQL shared_buffers | OS managed | Automatic LRU | > 90% |
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
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.
| Region | Primary Use | SMTP IPs | Compliance |
|---|---|---|---|
| us-east-1 | North America accounts | 50 dedicated IPs | CAN-SPAM, CCPA |
| eu-west-1 | EU accounts | 30 dedicated IPs | GDPR, ePrivacy |
| ap-southeast-1 | APAC accounts | 20 dedicated IPs | Local regulations |
| us-west-2 | DR standby for us-east-1 | 20 dedicated IPs | CAN-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.
| Component | Specification | Monthly Cost (USD) |
|---|---|---|
| Application Servers | 64x c6i.2xlarge (8 vCPU, 16GB) | ,000 |
| PostgreSQL (Primary + Replicas) | 16x r6i.4xlarge, 10TB gp3 storage | ,000 |
| Redis Cluster | 24x r6g.xlarge nodes, 500GB total | ,000 |
| Apache Kafka | 12x kafka.m5.2xlarge brokers | ,000 |
| Elasticsearch | 8x r6i.xlarge.search nodes, 4TB | ,000 |
| Object Storage (S3) | 100TB (images, assets, exports) | ,500 |
| CDN (CloudFront) | 50TB/month transfer, 100M requests | ,000 |
| SMTP Infrastructure | 120 dedicated IPs across 3 regions | ,000 |
| DNS (Route 53) | 600K hosted zones + queries | ,500 |
| Monitoring (Datadog) | 100 hosts, APM, logs, metrics | ,000 |
| Load Balancers | 6 ALBs across regions | ,000 |
| NAT Gateways | 6 NAT gateways across regions | ,000 |
| Staff (Engineering Team) | 40 engineers + 5 SREs | ,200,000 |
| Stock Image Licensing | Enterprise license (50K+ images) | ,000 |
| Email Validation Service | 1.5B contacts validated | ,000 |
| Third-Party APIs | Social, 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.
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.
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
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.