system-design50 min read

How to Design a Survey & Forms Builder Platform — A Senior+ Guide | Ayodhyya

How to Design a Survey & Forms Builder Platform

A complete system design guide covering form building, response collection, analytics, branching logic, integrations, and compliance — like Google Forms and Typeform at scale.

Senior+ System Design Guide 10,000+ Words Full Architecture & C# Code

1. Overview & Problem Statement

A survey and forms builder platform enables users to create, distribute, and analyze questionnaires — from simple contact forms to complex multi-branch surveys with conditional logic, quiz grading, and real-time analytics. Think Google Forms, Typeform, SurveyMonkey, JotForm, or Microsoft Forms.

Core Challenge: Build a platform that handles millions of concurrent respondents filling out forms, supports a rich drag-and-drop builder UI, processes conditional logic in real-time, collects files, computes analytics, and integrates with third-party tools — all with sub-second latency and 99.99% uptime.

Why This Is Hard

  • Schema Flexibility: Forms are dynamic schemas defined at runtime — there is no fixed database table for "form responses" because each form has a different shape.
  • Branching Logic: Skip logic, piping, and conditional blocks create a directed graph of question flow, not a linear sequence.
  • Real-Time Analytics: Form owners expect live dashboards as responses arrive, requiring event streaming and incremental aggregation.
  • File Uploads: Handling file uploads at scale (images, documents, PDFs) with virus scanning, storage, and access control.
  • Global Scale: Forms must load fast worldwide, with CDNs, edge caching, and geo-distributed response collection.

Platform Capabilities at a Glance

CapabilityDetails
Form BuilderDrag-and-drop, block-based, real-time preview
Question Types15+ types including text, choice, rating, NPS, matrix, file upload, date, dropdown
Branching LogicConditional skip, show/hide blocks, piping, calculated fields
ThemesCustom colors, fonts, logos, custom CSS injection
Response CollectionAnonymous & authenticated, partial save, multi-device
AnalyticsReal-time charts, completion rates, drop-off analysis
ExportCSV, Excel, Google Sheets integration
SharingPublic link, email campaigns, embed iframe, QR code
Quiz ModeAuto-grading, points, answer keys, time limits
IntegrationsWebhooks, Slack, Zapier, Google Sheets, Mailchimp
Spam ProtectionCAPTCHA, rate limiting, link expiration
ComplianceGDPR, data encryption, right to deletion

2. Functional & Non-Functional Requirements

Functional Requirements

RequirementDescription
FR-1Users can create forms with drag-and-drop builder
FR-2Support 15+ question types with rich configuration
FR-3Conditional branching and skip logic per question
FR-4Respondents can submit responses, including file uploads
FR-5Form owners view real-time analytics dashboards
FR-6Export responses to CSV, Excel, Google Sheets
FR-7Share forms via link, email, embed, QR code
FR-8Quiz mode with auto-grading and point allocation
FR-9Form scheduling (open/close dates) and response limits
FR-10Partial responses with save-and-continue via email link
FR-11Email notifications on each submission
FR-12Integrations with Slack, Zapier, webhooks, Google Sheets
FR-13Themes and branding (colors, fonts, logos)
FR-14Multi-language form support
FR-15Anonymous and authenticated response collection
FR-16Spam protection via CAPTCHA and rate limiting

Non-Functional Requirements

RequirementTarget
Availability99.99% uptime
Latency (form load)< 200ms p99 via CDN
Latency (submission)< 500ms p99
Scale (forms)100M+ forms created
Scale (responses)10B+ responses stored
Scale (concurrent)1M+ concurrent respondents
Analytics freshness< 5 seconds from submission
Data retentionConfigurable, default 2 years
SecurityEncryption at rest and in transit, RBAC
ComplianceGDPR, CCPA, SOC 2

3. Question Types

A forms builder must support a rich set of question types, each with its own configuration schema, rendering component, validation rules, and analytics aggregation strategy.

Complete Question Type Catalog

TypeConfig OptionsValidationAnalytics
Short Textplaceholder, maxLength, patternRequired, regex, max lengthWord cloud, text length distribution
Long Text (Paragraph)placeholder, maxLength, rowsRequired, max lengthWord cloud, sentiment
Multiple Choiceoptions[], allowOther, multiSelect, shuffleRequired, min/max selectionsBar chart, pie chart, option distribution
Checkboxesoptions[], allowOther, minSelect, maxSelectRequired, min/max countBar chart, co-occurrence matrix
Dropdownoptions[], searchable, multiSelectRequired, valid optionBar chart, option distribution
Linear Scale (Rating)min, max, minLabel, maxLabelRequiredAverage, histogram, distribution
Star RatingmaxStars (default 5)RequiredAverage, distribution
NPS (Net Promoter Score)lowLabel, highLabelRequiredNPS score (-100 to 100), distribution
DateincludeTime, minDate, maxDateRequired, date rangeTimeline histogram
Timeformat (12h/24h)RequiredDistribution
EmailplaceholderRequired, email regexCount, uniqueness
Numbermin, max, step, prefix, suffixRequired, rangeAverage, median, histogram
File UploadmaxSize, allowedTypes, maxFilesRequired, type, sizeCount, type distribution
Matrix (Grid)rows[], columns[], type (single/multi)Required per rowHeat map, per-row distribution
Phone NumbercountryCode, formatRequired, phone regexCount
Addressfields[] (street, city, state, zip, country)Required per fieldGeographic aggregation
SignaturepenColor, backgroundColorRequired (non-empty canvas)Count
Section Headertitle, descriptionN/AN/A
Image Choiceimages[], allowMultiSelectRequiredOption distribution

Question Type Configuration Schema

Each question type has a discriminated union configuration stored as JSON in the database. This allows the builder UI to render the correct editor component and the renderer to produce the correct respondent view.

C#
public abstract class QuestionConfig
{
    public string QuestionId { get; set; }
    public string Type { get; set; } // discriminator
    public string Title { get; set; }
    public string Description { get; set; }
    public bool IsRequired { get; set; }
    public int OrderIndex { get; set; }
    public string ValidationRuleJson { get; set; } // optional custom validation
}

public class MultipleChoiceConfig : QuestionConfig
{
    public List<OptionItem> Options { get; set; }
    public bool AllowOther { get; set; }
    public bool MultiSelect { get; set; }
    public int? MinSelections { get; set; }
    public int? MaxSelections { get; set; }
    public bool ShuffleOptions { get; set; }
}

public class RatingConfig : QuestionConfig
{
    public int Min { get; set; } = 1;
    public int Max { get; set; } = 5;
    public string MinLabel { get; set; }
    public string MaxLabel { get; set; }
}

public class NpsConfig : QuestionConfig
{
    public string LowLabel { get; set; } = "Not at all likely";
    public string HighLabel { get; set; } = "Extremely likely";
}

public class FileUploadConfig : QuestionConfig
{
    public long MaxFileSizeBytes { get; set; } = 10 * 1024 * 1024; // 10MB
    public List<string> AllowedMimeTypes { get; set; }
    public int MaxFiles { get; set; } = 1;
}

public class MatrixConfig : QuestionConfig
{
    public List<string> Rows { get; set; }
    public List<string> Columns { get; set; }
    public MatrixType MatrixType { get; set; }
}

public enum MatrixType { Single, Multi }

public class OptionItem
{
    public string Id { get; set; }
    public string Label { get; set; }
    public string ImageUrl { get; set; } // for image choice
}
Design Principle: Use a polymorphic JSON schema with a type discriminator field. This gives maximum flexibility for adding new question types without schema migrations while still allowing type-safe rendering on the client via discriminated unions.

4. Form Builder UI — Drag-and-Drop & Block-Based Editor

The form builder is the heart of the product. It must feel instant, intuitive, and powerful — supporting real-time preview, undo/redo, keyboard shortcuts, and rich configuration panels.

Builder Architecture

graph TB subgraph "Client-Side Builder (React/Blazor)" A[Form Canvas] --> B[Block Palette] A --> C[Property Panel] A --> D[Preview Panel] E[Undo/Redo Stack] --> A F[Form State Machine] --> A G[Auto-Save Manager] --> A end B --> B1[Text Block] B --> B2[Choice Block] B --> B3[Rating Block] B --> B4[File Block] B --> B5[Layout Block] B --> B6[Logic Block] F --> G G -->|REST PATCH| H[Form Service] H --> I[Form Schema Store] H --> J[Version History]

Drag-and-Drop Implementation

The builder uses a virtual list for question blocks to handle forms with 100+ questions without performance degradation. Each block is a self-contained component with its own state, validation, and configuration panel.

C# — Form Builder State Model
public class FormBuilderState
{
    public string FormId { get; set; }
    public string Title { get; set; }
    public string Description { get; set; }
    public List<QuestionBlock> Blocks { get; set; } = new();
    public FormTheme Theme { get; set; }
    public List<LogicRule> LogicRules { get; set; } = new();
    public FormSettings Settings { get; set; }
    public int Version { get; set; }
    public Stack<FormBuilderState> UndoStack { get; set; } = new();
    public Stack<FormBuilderState> RedoStack { get; set; } = new();

    public void MoveBlock(int fromIndex, int toIndex)
    {
        if (fromIndex == toIndex) return;
        SaveSnapshot();
        var block = Blocks[fromIndex];
        Blocks.RemoveAt(fromIndex);
        Blocks.Insert(toIndex, block);
        ReindexBlocks();
    }

    public void AddBlock(QuestionBlock block, int? insertAt = null)
    {
        SaveSnapshot();
        var index = insertAt ?? Blocks.Count;
        Blocks.Insert(index, block);
        ReindexBlocks();
    }

    public void RemoveBlock(string blockId)
    {
        SaveSnapshot();
        Blocks.RemoveAll(b => b.Id == blockId);
        ReindexBlocks();
    }

    private void SaveSnapshot()
    {
        UndoStack.Push(DeepClone(this));
        RedoStack.Clear();
    }

    private void ReindexBlocks()
    {
        for (int i = 0; i < Blocks.Count; i++)
            Blocks[i].OrderIndex = i;
    }

    private FormBuilderState DeepClone(FormBuilderState source)
    {
        return JsonSerializer.Deserialize<FormBuilderState>(
            JsonSerializer.Serialize(source));
    }
}

public class QuestionBlock
{
    public string Id { get; set; } = Guid.NewGuid().ToString();
    public string Type { get; set; }
    public QuestionConfig Config { get; set; }
    public int OrderIndex { get; set; }
    public bool IsVisible { get; set; } = true;
}

Auto-Save Strategy

The builder implements debounced auto-save with optimistic versioning. Every change triggers a debounced save (500ms after last keystroke) that sends a JSON patch to the server. The server maintains version history so form owners can roll back to any previous version.

Conflict Resolution: When multiple editors modify the same form simultaneously (collaborative editing), use Operational Transformation (OT) or CRDT-based merging on the block list. Each block has a unique ID, so operations can be composed: InsertBlock, MoveBlock, UpdateBlockConfig, DeleteBlock.

Preview System

The preview panel renders the form exactly as respondents will see it, including conditional logic simulation. The builder can toggle between desktop, tablet, and mobile preview widths. In preview mode, branching logic is active so the builder can test skip patterns end-to-end.

Block Configuration Panel

When a user clicks a block, the right panel slides in with type-specific configuration. For a multiple choice block, this includes: options list (with drag reorder), "allow other" toggle, shuffle toggle, selection limits, and validation rules. Changes are reflected in real-time on the canvas.

5. Data Model & Schema Design

The data model must handle dynamic schemas — each form defines its own structure, and responses are shapeless key-value maps validated against the form schema at write time.

Entity Relationship Diagram

erDiagram USERS ||--o{ FORMS : creates USERS ||--o{ RESPONSES : submits FORMS ||--o{ QUESTIONS : contains FORMS ||--o{ FORM_THEMES : has FORMS ||--o{ LOGIC_RULES : defines FORMS ||--o{ RESPONSES : receives FORMS ||--o{ FORM_SHARES : shared_via QUESTIONS ||--o{ QUESTION_OPTIONS : has RESPONSES ||--o{ ANSWERS : contains ANSWERS ||--o{ FILE_ATTACHMENTS : includes FORMS ||--o{ FORM_VERSIONS : versioned FORMS ||--o{ FORM_TRANSLATIONS : translated USERS { uuid id PK string email string name string password_hash datetime created_at string plan_tier } FORMS { uuid id PK uuid owner_id FK string title text description jsonb settings string status datetime created_at datetime updated_at int current_version } QUESTIONS { uuid id PK uuid form_id FK string type jsonb config int order_index bool is_required string validation_json } QUESTION_OPTIONS { uuid id PK uuid question_id FK string label string image_url int order_index } FORM_THEMES { uuid id PK uuid form_id FK string primary_color string font_family string background_url jsonb custom_css } LOGIC_RULES { uuid id PK uuid form_id FK string source_question_id string condition_type jsonb condition_value string action_type jsonb action_value int priority } RESPONSES { uuid id PK uuid form_id FK uuid user_id FK nullable string session_id string status jsonb metadata datetime started_at datetime submitted_at float score } ANSWERS { uuid id PK uuid response_id FK uuid question_id FK jsonb value int time_spent_ms } FILE_ATTACHMENTS { uuid id PK uuid answer_id FK string file_name string storage_key long file_size string mime_type datetime uploaded_at } FORM_SHARES { uuid id PK uuid form_id FK string share_type string share_token jsonb settings datetime created_at } FORM_VERSIONS { uuid id PK uuid form_id FK int version jsonb schema_snapshot datetime created_at uuid created_by FK } FORM_TRANSLATIONS { uuid id PK uuid form_id FK string locale jsonb translations }

Response Schema — The Dynamic Answer Store

Since each form has a unique schema, responses are stored as JSON documents. Each answer maps a question ID to its typed value. This is the most critical design decision — we trade strict schema validation for flexibility, enforcing correctness at the API layer.

C# — Response & Answer Models
public class Response
{
    public Guid Id { get; set; }
    public Guid FormId { get; set; }
    public Guid? UserId { get; set; } // null for anonymous
    public string SessionId { get; set; }
    public ResponseStatus Status { get; set; }
    public Dictionary<string, object> Metadata { get; set; }
    public DateTime StartedAt { get; set; }
    public DateTime? SubmittedAt { get; set; }
    public double? Score { get; set; } // for quiz mode
    public List<Answer> Answers { get; set; } = new();
}

public class Answer
{
    public Guid Id { get; set; }
    public Guid QuestionId { get; set; }
    public AnswerValue Value { get; set; }
    public int TimeSpentMs { get; set; }
}

[JsonConverter(typeof(JsonSubtypeConverter))]
[JsonSubtypeFallbackType(typeof(AnswerValue))]
public abstract class AnswerValue { }

public class TextAnswer : AnswerValue
{
    public string Text { get; set; }
}

public class ChoiceAnswer : AnswerValue
{
    public List<string> SelectedOptionIds { get; set; }
    public string OtherText { get; set; }
}

public class RatingAnswer : AnswerValue
{
    public int Rating { get; set; }
}

public class NumberAnswer : AnswerValue
{
    public decimal Number { get; set; }
}

public class DateAnswer : AnswerValue
{
    public DateTime Date { get; set; }
}

public class FileAnswer : AnswerValue
{
    public List<FileAttachment> Files { get; set; }
}

public class MatrixAnswer : AnswerValue
{
    public Dictionary<string, string> RowSelections { get; set; }
    // rowId -> selectedColumnId
}

public class AddressAnswer : AnswerValue
{
    public string Street { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public string ZipCode { get; set; }
    public string Country { get; set; }
}

public enum ResponseStatus
{
    InProgress,
    Completed,
    PartiallyCompleted
}

Storage Strategy

EntityPrimary StoreReason
FormsPostgreSQLRelational, ACID for schema edits
Questions/OptionsPostgreSQLPart of form schema, needs transactions
ResponsesPostgreSQL (partitioned) + S3 (archive)Queryable, partitioned by form_id and date
AnswersPostgreSQL (JSONB column)Flexible schema, indexed for analytics
File AttachmentsS3 / Azure BlobBinary objects, CDN-cacheable
Form Schema SnapshotsPostgreSQLVersion history
Analytics AggregatesRedis + ClickHouseReal-time counters + OLAP queries
Form Content (rendered)CDN Edge CacheLow-latency delivery worldwide

Response Partitioning

At scale, the responses table must be partitioned. The optimal strategy is composite partitioning: first by form_id hash (for query locality), then by submission date (for time-range queries and archival).

SQL — Partitioned Response Table
CREATE TABLE responses (
    id UUID PRIMARY KEY,
    form_id UUID NOT NULL,
    user_id UUID,
    session_id VARCHAR(128),
    status VARCHAR(20),
    metadata JSONB,
    started_at TIMESTAMPTZ NOT NULL,
    submitted_at TIMESTAMPTZ,
    score DOUBLE PRECISION,
    created_at TIMESTAMPTZ DEFAULT NOW()
) PARTITION BY HASH (form_id);

CREATE TABLE responses_p0 PARTITION OF responses
    FOR VALUES WITH (MODULUS 64, REMAINDER 0);
CREATE TABLE responses_p1 PARTITION OF responses
    FOR VALUES WITH (MODULUS 64, REMAINDER 1);
-- ... through p63

CREATE INDEX idx_responses_form_submitted
    ON responses (form_id, submitted_at);
CREATE INDEX idx_responses_user
    ON responses (user_id) WHERE user_id IS NOT NULL;

6. High-Level Architecture

graph TB subgraph "Client Layer" C1[Web App React/Blazor] C2[Mobile Responsive PWA] C3[Embed Widget iframe] end subgraph "CDN and Edge" CDN[CloudFront / Cloudflare] EDGE[Edge Cache Form Schemas] end subgraph "API Gateway" GW[API Gateway Rate Limiting Auth] end subgraph "Core Services" FS[Form Service] RS[Response Service] AS[Analytics Service] US[User Service] NS[Notification Service] IS[Integration Service] US3[Upload Service] LS[Logic Engine Service] end subgraph "Background Workers" W1[Export Worker] W2[Analytics Aggregator] W3[Email Sender] W4[Webhook Dispatcher] W5[File Processor] W6[Spam Detector] end subgraph "Data Layer" PG[(PostgreSQL Cluster)] RD[(Redis Cluster)] S3[(S3 / Blob Storage)] CH[(ClickHouse)] MQ[Message Queue Kafka/RabbitMQ] end C1 --> CDN C2 --> CDN C3 --> CDN CDN --> EDGE CDN --> GW GW --> FS GW --> RS GW --> AS GW --> US GW --> US3 FS --> PG FS --> RD FS --> MQ RS --> PG RS --> MQ RS --> S3 AS --> CH AS --> RD US --> PG US3 --> S3 NS --> MQ IS --> MQ MQ --> W1 MQ --> W2 MQ --> W3 MQ --> W4 MQ --> W5 MQ --> W6

Service Responsibilities

ServiceResponsibilityScaling Strategy
Form ServiceCRUD for forms, schema management, versioning, sharingHorizontal, read replicas for form reads
Response ServiceAccept submissions, validate against schema, partial savesSharded by form_id, write-optimized
Analytics ServiceReal-time aggregation, dashboard data, completion statsClickHouse for OLAP, Redis for counters
User ServiceAuthentication, profiles, team management, billingStandard horizontal scaling
Notification ServiceEmail on submission, form alerts, digest emailsAsync via message queue
Integration ServiceWebhook dispatch, Slack/Zapier connectors, Google Sheets syncWorker pool with rate limiting
Upload ServicePresigned URL generation, file validation, virus scanningStateless, S3-native
Logic EngineEvaluate conditional branching rules per responseStateless, embeddable in response service
Key Insight: The form rendering and response submission paths must be separated. Form rendering is a read-heavy operation (cacheable, CDN-friendly), while response submission is write-heavy (requires validation, analytics triggering, notifications). This read-write separation is fundamental to scaling independently.

7. API Design

The API follows RESTful conventions with some extensions for complex operations like conditional logic evaluation and bulk analytics queries.

Form Management APIs

MethodEndpointDescription
POST/api/v1/formsCreate a new form
GET/api/v1/formsList user's forms (paginated)
GET/api/v1/forms/{id}Get form schema (builder view)
PATCH/api/v1/forms/{id}Update form metadata
PUT/api/v1/forms/{id}/schemaReplace full form schema (auto-versioned)
PATCH/api/v1/forms/{id}/schemaPartial schema update (JSON patch)
DELETE/api/v1/forms/{id}Soft delete form
POST/api/v1/forms/{id}/publishPublish form (makes it live)
POST/api/v1/forms/{id}/closeClose form (stops accepting responses)
GET/api/v1/forms/{id}/versionsList schema versions
GET/api/v1/forms/{id}/versions/{v}Get specific version snapshot
POST/api/v1/forms/{id}/duplicateClone form

Response Collection APIs

MethodEndpointDescription
POST/api/v1/forms/{id}/responsesSubmit complete response
PUT/api/v1/forms/{id}/responses/{rid}/partialSave partial response
GET/api/v1/forms/{id}/responses/{rid}Get single response (auth required)
GET/api/v1/forms/{id}/responsesList responses (paginated, filtered)
DELETE/api/v1/forms/{id}/responses/{rid}Delete a response
POST/api/v1/forms/{id}/responses/exportTrigger export (returns job ID)

Public (Respondent) APIs

MethodEndpointDescription
GET/api/v1/public/forms/{token}Get rendered form (no auth, CDN-cached)
POST/api/v1/public/forms/{token}/responsesSubmit response (anonymous or with auth)
PUT/api/v1/public/forms/{token}/responses/{sid}/partialSave partial progress
GET/api/v1/public/forms/{token}/responses/{sid}Resume partial response

Analytics APIs

MethodEndpointDescription
GET/api/v1/forms/{id}/analytics/summaryResponse count, completion rate, avg time
GET/api/v1/forms/{id}/analytics/questionsPer-question analytics
GET/api/v1/forms/{id}/analytics/dropoffQuestion-by-question drop-off funnel
GET/api/v1/forms/{id}/analytics/timelineResponses over time (time-series)
WS/ws/forms/{id}/analytics/liveWebSocket for real-time dashboard updates

Example — Submit Response Endpoint

C# — Response Submission
[ApiController]
[Route("api/v1/public/forms")]
public class PublicResponseController : ControllerBase
{
    private readonly IResponseService _responseService;
    private readonly IFormSchemaValidator _schemaValidator;
    private readonly ILogicEngine _logicEngine;
    private readonly IEventBus _eventBus;

    [HttpPost("{formToken}/responses")]
    public async Task<ActionResult<SubmitResponseResult>> SubmitResponse(
        string formToken,
        [FromBody] SubmitResponseRequest request)
    {
        var form = await _responseService.GetPublishedFormAsync(formToken);
        if (form == null) return NotFound("Form not found or not published");

        if (!IsFormOpen(form))
            return BadRequest("Form is not currently accepting responses");

        if (await _responseService.HasReachedLimitAsync(form.Id))
            return Conflict("Response limit reached");

        var validationErrors = _schemaValidator.Validate(form.Schema, request.Answers);
        if (validationErrors.Any())
            return BadRequest(new { Errors = validationErrors });

        var processedAnswers = _logicEngine.ProcessAnswers(form, request.Answers);

        var response = new Response
        {
            FormId = form.Id,
            SessionId = request.SessionId,
            Status = ResponseStatus.Completed,
            StartedAt = request.StartedAt,
            SubmittedAt = DateTime.UtcNow,
            Metadata = request.Metadata,
            Answers = processedAnswers
        };

        if (form.Settings.IsQuizMode)
            response.Score = _logicEngine.CalculateScore(form.Schema, processedAnswers);

        var responseId = await _responseService.SaveResponseAsync(response);

        await _eventBus.PublishAsync(new ResponseSubmittedEvent
        {
            ResponseId = responseId,
            FormId = form.Id,
            SubmittedAt = response.SubmittedAt.Value,
            Score = response.Score
        });

        return Ok(new SubmitResponseResult
        {
            ResponseId = responseId,
            Score = response.Score,
            ThankYouMessage = form.Settings.ThankYouMessage
        });
    }
}

8. Form Logic & Branching

Conditional logic transforms a linear form into a dynamic decision tree. Respondents see only relevant questions based on their previous answers, creating personalized experiences and improving completion rates.

Logic Rule Types

Rule TypeDescriptionExample
Skip LogicSkip to a specific question based on answerIf Q3 = "No", skip to Q7
Conditional VisibilityShow/hide a question based on answerShow Q5 only if Q4 = "Other"
BranchingNavigate to different sectionsIf satisfaction < 3, go to feedback section
End FormEnd the form early based on answerIf "Not interested", end immediately
PipingInsert previous answer into question text"You said {Q2}. Why?"
CalculationsComputed fields from numeric answersTotal = Q1 + Q2 + Q3
Quota LogicStop accepting based on quotasMax 100 responses from age group 18-24

Logic Rule Schema

C# — Logic Rule Models
public class LogicRule
{
    public string Id { get; set; }
    public string FormId { get; set; }
    public string SourceQuestionId { get; set; }
    public LogicCondition Condition { get; set; }
    public LogicAction Action { get; set; }
    public int Priority { get; set; } // lower = evaluated first
}

public abstract class LogicCondition
{
    public string QuestionId { get; set; }
}

public class EqualsCondition : LogicCondition
{
    public string OptionId { get; set; }
    public string OtherText { get; set; }
}

public class ContainsCondition : LogicCondition
{
    public List<string> OptionIds { get; set; }
}

public class NumericCondition : LogicCondition
{
    public NumericOperator Operator { get; set; }
    public decimal Value { get; set; }
}

public class TextCondition : LogicCondition
{
    public TextOperator Operator { get; set; }
    public string Value { get; set; }
}

public enum NumericOperator { Lt, Gt, Eq, Lte, Gte, Ne }
public enum TextOperator { Contains, NotContains, StartsWith, EndsWith, Regex }

public abstract class LogicAction { }

public class SkipToAction : LogicAction
{
    public string TargetQuestionId { get; set; }
}

public class ShowQuestionAction : LogicAction
{
    public string TargetQuestionId { get; set; }
}

public class HideQuestionAction : LogicAction
{
    public string TargetQuestionId { get; set; }
}

public class EndFormAction : LogicAction { }

public class JumpToSectionAction : LogicAction
{
    public string TargetSectionId { get; set; }
}

Evaluating Logic at Submission Time

C# — Logic Engine
public class LogicEngine : ILogicEngine
{
    public List<Answer> ProcessAnswers(FormSchema form, List<Answer> answers)
    {
        var answerMap = answers.ToDictionary(a => a.QuestionId);
        var rules = form.LogicRules.OrderBy(r => r.Priority).ToList();
        var hiddenQuestions = new HashSet<string>();
        var processedAnswers = new List<Answer>();

        foreach (var question in form.Questions.OrderBy(q => q.OrderIndex))
        {
            if (hiddenQuestions.Contains(question.Id))
                continue;

            if (!answerMap.TryGetValue(question.Id, out var answer))
                continue;

            var triggeredRules = rules.Where(r =>
                r.SourceQuestionId == question.Id &&
                EvaluateCondition(r.Condition, answerMap));

            foreach (var rule in triggeredRules)
            {
                switch (rule.Action)
                {
                    case SkipToAction skip:
                        HideQuestionsInRange(form, question.Id,
                            skip.TargetQuestionId, hiddenQuestions);
                        break;
                    case HideQuestionAction hide:
                        hiddenQuestions.Add(hide.TargetQuestionId);
                        break;
                    case ShowQuestionAction show:
                        hiddenQuestions.Remove(show.TargetQuestionId);
                        break;
                    case EndFormAction:
                        return processedAnswers;
                }
            }

            processedAnswers.Add(answer);
        }

        return processedAnswers;
    }

    private bool EvaluateCondition(LogicCondition condition,
        Dictionary<string, Answer> answers)
    {
        if (!answers.TryGetValue(condition.QuestionId, out var answer))
            return false;

        return condition switch
        {
            EqualsCondition eq => answer.Value is ChoiceAnswer c
                && c.SelectedOptionIds.Contains(eq.OptionId),
            ContainsCondition cc => answer.Value is ChoiceAnswer c2
                && cc.OptionIds.All(id => c2.SelectedOptionIds.Contains(id)),
            NumericComparison nc => answer.Value is NumberAnswer n
                && CompareNumeric(nc.Operator, n.Number, nc.Value),
            TextCondition tc => answer.Value is TextAnswer t
                && EvaluateTextCondition(tc.Operator, t.Text, tc.Value),
            _ => false
        };
    }
}
Performance: For forms with many branching rules, precompute the skip graph on publish. At submission time, the engine only evaluates the minimum set of rules by traversing the precomputed graph rather than scanning all rules.

9. Themes & Branding

Themes allow form creators to match their brand identity. The system supports predefined themes, custom color/font overrides, background images, and optional CSS injection for advanced users.

Theme Configuration

C# — Theme Model
public class FormTheme
{
    public string PrimaryColor { get; set; } = "#4285f4";
    public string SecondaryColor { get; set; } = "#34a853";
    public string BackgroundColor { get; set; } = "#ffffff";
    public string TextColor { get; set; } = "#202124";
    public string FontFamily { get; set; } = "Google Sans, Roboto, sans-serif";
    public string HeaderFontFamily { get; set; }
    public string BackgroundImageUrl { get; set; }
    public BackgroundPosition BackgroundPosition { get; set; }
    public string LogoUrl { get; set; }
    public int? LogoMaxWidth { get; set; }
    public string CustomCss { get; set; }
    public bool CoverPage { get; set; }
    public string ProgressBarColor { get; set; }
    public ButtonStyle ButtonStyle { get; set; }
    public string HeaderAlignment { get; set; }
}

public enum BackgroundPosition { Cover, Tile, Center, Stretch }
public enum ButtonStyle { Rounded, Square, Pill }

public class PredefinedTheme
{
    public string Id { get; set; }
    public string Name { get; set; }
    public string PreviewImageUrl { get; set; }
    public FormTheme Theme { get; set; }
}

Theme Application Pipeline

Themes are applied during form rendering. The renderer combines the form schema with the theme configuration to produce the final HTML/CSS. Custom CSS is sanitized (CSS injection prevention) using a whitelist of safe CSS properties before inclusion.

10. Response Collection & Storage

Response collection is the most write-intensive part of the system. Every submission must be validated, stored, and trigger downstream events (analytics, notifications, integrations) within strict latency requirements.

Submission Flow

sequenceDiagram participant R as Respondent participant CDN as CDN Edge participant API as API Gateway participant RS as Response Service participant DB as PostgreSQL participant MQ as Message Queue participant AN as Analytics Worker participant NT as Notification Worker participant IN as Integration Worker R->>CDN: GET /forms/token CDN-->>R: Form HTML cached R->>API: POST /forms/token/responses API->>RS: Validate and process RS->>DB: INSERT response and answers RS->>MQ: Publish ResponseSubmittedEvent RS-->>API: 201 Created API-->>R: Submission confirmed MQ->>AN: Update real-time counters MQ->>NT: Send email notification MQ->>IN: Trigger webhooks/integrations

Write Path Optimization

  • Batched Writes: For high-volume forms, responses are batched before database writes (micro-batching within 100ms windows).
  • Write-Ahead for Analytics: Analytics counters in Redis are updated synchronously before the database write, ensuring the dashboard is always at least as up-to-date as the database.
  • Async Downstream: Notifications, integrations, and export triggers are fully asynchronous via message queue.
  • Connection Pooling: PgBouncer or similar connection pooler in front of PostgreSQL to handle connection spikes.

Concurrent Submission Handling

C# — Response Service
public class ResponseService : IResponseService
{
    private readonly AppDbContext _db;
    private readonly IRedisCache _cache;
    private readonly IEventBus _eventBus;

    public async Task<Guid> SaveResponseAsync(Response response)
    {
        var countKey = $"form:{response.FormId}:response_count";
        await _cache.IncrementAsync(countKey);

        await using var transaction = await _db.Database.BeginTransactionAsync();
        try
        {
            _db.Responses.Add(response);
            await _db.SaveChangesAsync();
            await transaction.CommitAsync();
        }
        catch (DbUpdateException)
        {
            await transaction.RollbackAsync();
            throw new ResponseConflictException("Duplicate submission detected");
        }

        await _eventBus.PublishAsync(new ResponseSubmittedEvent
        {
            ResponseId = response.Id,
            FormId = response.FormId,
            SubmittedAt = response.SubmittedAt.Value,
            Score = response.Score
        });

        return response.Id;
    }
}

11. Real-Time Response Analytics

Form owners expect live dashboards showing response counts, completion rates, and per-question breakdowns as they happen. This requires a streaming analytics architecture.

Analytics Architecture

graph LR subgraph "Ingestion" E1[ResponseSubmittedEvent] --> K[Kafka / RabbitMQ] end subgraph "Real-Time Layer" K --> W1[Analytics Worker] W1 --> RD[(Redis)] W1 --> WS[WebSocket Hub] WS --> DASH[Dashboard Client] end subgraph "Batch Layer" K --> W2[Batch Aggregator] W2 --> CH[(ClickHouse)] W2 --> PG[(PostgreSQL)] end subgraph "Queries" CH --> API[Analytics API] RD --> API API --> DASH end

Real-Time Counter Model

C# — Analytics Aggregator
public class AnalyticsWorker : IEventHandler<ResponseSubmittedEvent>
{
    private readonly IRedisCache _redis;

    public async Task HandleAsync(ResponseSubmittedEvent evt)
    {
        var formId = evt.FormId.ToString();

        await _redis.IncrementAsync($"analytics:{formId}:total_responses");
        await _redis.IncrementAsync($"analytics:{formId}:completed_responses");

        var hourBucket = evt.SubmittedAt.ToString("yyyyMMddHH");
        await _redis.IncrementAsync($"analytics:{formId}:timeline:{hourBucket}");

        foreach (var answer in evt.Answers)
        {
            var qKey = $"analytics:{formId}:q:{answer.QuestionId}";
            if (answer.Value is ChoiceAnswer choice)
            {
                foreach (var optionId in choice.SelectedOptionIds)
                    await _redis.HashIncrementAsync($"{qKey}:options", optionId);
            }
            else if (answer.Value is RatingAnswer rating)
            {
                await _redis.ListRightPushAsync($"{qKey}:ratings", rating.Rating);
                await _redis.IncrementAsync($"{qKey}:sum");
            }
        }

        await _redis.PublishAsync($"form:{formId}:live",
            JsonSerializer.Serialize(evt));
    }
}

Analytics Dashboard Components

ComponentData SourceRefresh Rate
Total ResponsesRedis counterReal-time (WebSocket push)
Completion RateRedis (completed / started)Real-time
Average Completion TimeClickHouse aggregate30-second polling
Response TimelineClickHouse time-series1-minute refresh
Per-Question DistributionRedis hash + ClickHouseReal-time for recent, batch for historical
NPS ScoreClickHouse aggregate1-minute refresh
Drop-off FunnelClickHouse5-minute refresh
Text Responses (Word Cloud)ClickHouse + NLP pipelineHourly
Device/Browser StatsClickHouseHourly
Geographic DistributionClickHouse + GeoIPHourly

Drop-Off Analysis

Drop-off analysis shows where respondents abandon the form. Each question tracks how many respondents reached it versus how many progressed to the next. This is computed by comparing the order of answers within each response.

Business Value: Drop-off analytics are the number one actionable insight for form optimization. If 40% of respondents abandon at Q7, the form owner knows to simplify or remove that question.

12. Response Export — CSV, Excel, Google Sheets

Export is a resource-intensive operation for forms with millions of responses. It must be handled asynchronously with progress tracking and delivery via download link or direct cloud integration.

Export Architecture

sequenceDiagram participant User as Form Owner participant API as Export API participant Q as Job Queue participant W as Export Worker participant S3 as S3 Storage participant Email as Email Service User->>API: POST /forms/id/responses/export API->>Q: Enqueue export job API-->>User: Job ID 202 Accepted User->>API: GET /export-jobs/jobId API-->>User: processing progress 0.45 Q->>W: Dequeue export job W->>W: Stream responses from DB W->>W: Write CSV/Excel in chunks W->>S3: Upload completed file W->>Email: Send download link User->>Email: Receive download link

Export Worker Implementation

C# — Export Worker
public class ExportWorker : IJobHandler<ExportJob>
{
    private readonly AppDbContext _db;
    private readonly IBlobStorage _storage;

    public async Task HandleAsync(ExportJob job)
    {
        var form = await _db.Forms
            .Include(f => f.Questions)
            .FirstOrDefaultAsync(f => f.Id == job.FormId);

        var fileName = $"{form.Title}_responses_{DateTime.UtcNow:yyyyMMdd}.{job.Format}";
        var tempPath = Path.GetTempFileName();

        await using var stream = File.Create(tempPath);
        await using var writer = new StreamWriter(stream);

        var headers = form.Questions
            .OrderBy(q => q.OrderIndex)
            .Select(q => q.Title)
            .ToList();
        headers.Insert(0, "Response ID");
        headers.Insert(1, "Submitted At");
        headers.Insert(2, "Status");
        await writer.WriteLineAsync(string.Join(",", headers.Select(EscapeCsv)));

        var batchSize = 1000;
        long offset = 0;
        long totalResponses = await _db.Responses
            .CountAsync(r => r.FormId == job.FormId
                && r.Status == ResponseStatus.Completed);

        while (true)
        {
            var batch = await _db.Responses
                .Where(r => r.FormId == job.FormId
                    && r.Status == ResponseStatus.Completed)
                .OrderBy(r => r.SubmittedAt)
                .Skip((int)offset)
                .Take(batchSize)
                .ToListAsync();

            if (!batch.Any()) break;

            foreach (var response in batch)
            {
                var row = new List<string>
                {
                    response.Id.ToString(),
                    response.SubmittedAt?.ToString("yyyy-MM-dd HH:mm:ss") ?? "",
                    response.Status.ToString()
                };

                foreach (var question in form.Questions.OrderBy(q => q.OrderIndex))
                {
                    var answer = response.Answers
                        .FirstOrDefault(a => a.QuestionId == question.Id);
                    row.Add(answer != null ? FormatAnswerValue(answer.Value) : "");
                }

                await writer.WriteLineAsync(string.Join(",", row.Select(EscapeCsv)));
            }

            offset += batch.Count;
            await UpdateJobProgressAsync(job.Id, (double)offset / totalResponses);
        }

        var s3Key = $"exports/{job.Id}/{fileName}";
        await _storage.UploadAsync(s3Key, tempPath);
        File.Delete(tempPath);

        var downloadUrl = await _storage.GetPresignedUrlAsync(s3Key,
            TimeSpan.FromHours(24));
        await NotifyExportCompleteAsync(job, downloadUrl);
    }
}

Google Sheets Integration

For Google Sheets sync, the export worker uses the Google Sheets API v4 to create or update a spreadsheet. A background job keeps the sheet in sync by polling for new responses every 5 minutes and appending rows incrementally.

13. Form Sharing — Link, Email, Embed, QR Code

Forms must be distributable through multiple channels, each with specific requirements for tracking, authentication, and presentation.

Sharing Methods

MethodImplementationTracking
Public LinkHTTPS://forms.yourapp.com/f/{token}Views, unique visitors (cookie/IP)
Email InvitationPersonalized links with UTM parametersEmail open rate, click-through, per-recipient tracking
Embed (iframe)iframe src with CORS headers, responsive sizingEmbed page views, referrer tracking
QR CodeServer-generated QR code PNG/SVG from form URLSame as public link tracking
Social SharePre-composed tweets/posts with form linkSocial click-through
API EmbedJavaScript widget for custom integrationWidget load count, referrer

Email Campaign System

C# — Email Invitation Service
public class EmailInvitationService : IEmailInvitationService
{
    private readonly IEmailSender _emailSender;
    private readonly ITemplateEngine _templateEngine;

    public async Task SendInvitationsAsync(
        Guid formId,
        List<EmailRecipient> recipients,
        string customMessage)
    {
        var form = await _formService.GetFormAsync(formId);

        foreach (var recipient in recipients)
        {
            var trackingId = Guid.NewGuid().ToString();
            var personalToken = GeneratePersonalToken(form.Id, recipient.Email);
            var formUrl = $"https://forms.yourapp.com/f/{form.Token}?ref=email&rid={personalToken}";

            var html = await _templateEngine.RenderAsync("invitation-email", new
            {
                RecipientName = recipient.Name,
                FormTitle = form.Title,
                FormDescription = form.Description,
                FormUrl = formUrl,
                CustomMessage = customMessage,
                SenderName = form.OwnerName
            });

            await _emailSender.SendAsync(new EmailMessage
            {
                To = recipient.Email,
                Subject = $"{form.OwnerName} invited you to respond: {form.Title}",
                HtmlBody = html,
                Headers = new Dictionary<string, string>
                {
                    ["X-Tracking-Id"] = trackingId,
                    ["X-Form-Id"] = form.Id.ToString()
                }
            });

            await _db.Invitations.AddAsync(new EmailInvitation
            {
                FormId = formId,
                RecipientEmail = recipient.Email,
                TrackingId = trackingId,
                SentAt = DateTime.UtcNow,
                Status = InvitationStatus.Sent
            });
        }

        await _db.SaveChangesAsync();
    }
}

14. Form Validation

Validation happens at two layers: client-side (immediate UX feedback) and server-side (authoritative enforcement). Both layers share the same validation rules to ensure consistency.

Validation Rule Types

RuleApplies ToExample
RequiredAll typesQuestion must be answered
Min LengthTextAt least 10 characters
Max LengthTextNo more than 500 characters
Regex PatternText, Email, PhonePhone: ^\+?[1-9]\d{1,14}$
Min SelectionsCheckboxesSelect at least 2 options
Max SelectionsCheckboxes, Multi-selectSelect no more than 5
Numeric RangeNumberBetween 1 and 100
Date RangeDateMust be in the past
File TypeFile UploadOnly PDF, JPG, PNG
File SizeFile UploadMax 10MB per file
Custom JavaScriptAll typesCustom validation function

Server-Side Validation

C# — Schema Validator
public class FormSchemaValidator : IFormSchemaValidator
{
    public List<ValidationError> Validate(
        FormSchema schema, List<Answer> answers)
    {
        var errors = new List<ValidationError>();
        var answerMap = answers.ToDictionary(a => a.QuestionId);

        foreach (var question in schema.Questions)
        {
            if (question.IsHiddenByLogic) continue;

            answerMap.TryGetValue(question.Id, out var answer);

            if (question.Config.IsRequired
                && (answer == null || IsEmpty(answer.Value)))
            {
                errors.Add(new ValidationError
                {
                    QuestionId = question.Id,
                    Message = $"Question '{question.Title}' is required",
                    ErrorCode = "REQUIRED"
                });
                continue;
            }

            if (answer == null) continue;

            var typeErrors = question.Config switch
            {
                TextConfig tc => ValidateText(answer, tc),
                MultipleChoiceConfig mc => ValidateChoice(answer, mc),
                NumberConfig nc => ValidateNumber(answer, nc),
                FileUploadConfig fc => ValidateFile(answer, fc),
                DateConfig dc => ValidateDate(answer, dc),
                EmailConfig ec => ValidateEmail(answer, ec),
                _ => Enumerable.Empty<ValidationError>()
            };

            errors.AddRange(typeErrors);

            if (!string.IsNullOrEmpty(question.Config.ValidationRuleJson))
            {
                var customErrors = EvaluateCustomValidation(
                    answer, question.Config.ValidationRuleJson);
                errors.AddRange(customErrors);
            }
        }

        return errors;
    }

    private IEnumerable<ValidationError> ValidateText(
        Answer answer, TextConfig config)
    {
        if (answer.Value is not TextAnswer text) yield break;

        if (config.MaxLength.HasValue
            && text.Text.Length > config.MaxLength)
            yield return new ValidationError
            {
                QuestionId = answer.QuestionId,
                Message = $"Text must be no longer than {config.MaxLength} characters",
                ErrorCode = "MAX_LENGTH"
            };

        if (!string.IsNullOrEmpty(config.Pattern)
            && !Regex.IsMatch(text.Text, config.Pattern))
            yield return new ValidationError
            {
                QuestionId = answer.QuestionId,
                Message = config.PatternErrorMessage ?? "Invalid format",
                ErrorCode = "REGEX"
            };
    }

    private IEnumerable<ValidationError> ValidateChoice(
        Answer answer, MultipleChoiceConfig config)
    {
        if (answer.Value is not ChoiceAnswer choice) yield break;

        if (config.MinSelections.HasValue
            && choice.SelectedOptionIds.Count < config.MinSelections)
            yield return new ValidationError
            {
                QuestionId = answer.QuestionId,
                Message = $"Select at least {config.MinSelections} option(s)",
                ErrorCode = "MIN_SELECTIONS"
            };

        if (config.MaxSelections.HasValue
            && choice.SelectedOptionIds.Count > config.MaxSelections)
            yield return new ValidationError
            {
                QuestionId = answer.QuestionId,
                Message = $"Select no more than {config.MaxSelections} option(s)",
                ErrorCode = "MAX_SELECTIONS"
            };
    }

    private bool IsEmpty(AnswerValue value) => value switch
    {
        TextAnswer t => string.IsNullOrWhiteSpace(t.Text),
        ChoiceAnswer c => !c.SelectedOptionIds.Any(),
        NumberAnswer n => false,
        RatingAnswer r => false,
        FileAnswer f => !f.Files.Any(),
        _ => true
    };
}

15. File Upload Handling

File uploads in forms must handle images, documents, spreadsheets, and other file types securely. The system must scan for viruses, enforce size limits, and store files durably.

Upload Flow

sequenceDiagram participant R as Respondent participant API as Upload API participant S3 as S3 Storage participant Q as Virus Scan Queue participant AV as Antivirus Worker R->>API: Request presigned upload URL API->>API: Validate file type and size limits API->>S3: Generate presigned PUT URL API-->>R: uploadUrl, fileId R->>S3: PUT file directly bypasses server S3-->>R: 200 OK R->>API: Confirm upload fileId API->>Q: Enqueue virus scan API-->>R: Upload confirmed pending scan Q->>AV: Scan file AV->>S3: Download and scan AV-->>API: Scan result clean or infected

Presigned URL Upload

C# — Upload Service
public class UploadService : IUploadService
{
    private readonly IBlobStorage _storage;
    private readonly IVirusScanner _virusScanner;
    private readonly AppDbContext _db;

    public async Task<PresignedUploadResponse> GenerateUploadUrlAsync(
        Guid formId, string fileName, string contentType, long fileSize)
    {
        var question = await _db.Questions
            .OfType<FileUploadConfig>()
            .FirstOrDefaultAsync(q => q.FormId == formId);

        if (question == null)
            throw new InvalidOperationException(
                "Form does not accept file uploads");

        if (!question.AllowedMimeTypes.Contains(contentType))
            throw new ValidationException(
                $"File type '{contentType}' is not allowed");

        if (fileSize > question.MaxFileSizeBytes)
            throw new ValidationException(
                $"File size exceeds maximum of {question.MaxFileSizeBytes / 1024 / 1024}MB");

        var extension = Path.GetExtension(fileName);
        var storageKey = $"form-uploads/{formId}/{Guid.NewGuid()}{extension}";

        var uploadUrl = await _storage.GeneratePresignedUrlAsync(
            storageKey, HttpMethod.Put, TimeSpan.FromMinutes(15),
            new Dictionary<string, string>
            {
                ["Content-Type"] = contentType,
                ["Content-Length"] = fileSize.ToString()
            });

        var attachment = new FileAttachment
        {
            FileName = fileName,
            StorageKey = storageKey,
            FileSize = fileSize,
            MimeType = contentType,
            Status = FileStatus.Uploaded
        };
        _db.FileAttachments.Add(attachment);
        await _db.SaveChangesAsync();

        return new PresignedUploadResponse
        {
            FileId = attachment.Id,
            UploadUrl = uploadUrl,
            StorageKey = storageKey,
            ExpiresAt = DateTime.UtcNow.AddMinutes(15)
        };
    }

    public async Task ConfirmUploadAsync(Guid fileId)
    {
        var file = await _db.FileAttachments.FindAsync(fileId);
        file.Status = FileStatus.PendingScan;
        await _db.SaveChangesAsync();
        await _virusScanner.EnqueueScanAsync(file.StorageKey, fileId);
    }
}
Security: Files are never served directly from S3. Access is mediated through signed URLs with short expiry, and all file serves are logged for audit. Executable file types (.exe, .bat, .sh) are always rejected regardless of form configuration.

16. Anonymous vs Authenticated Responses

Forms can collect responses from anonymous users or require authentication. This is a per-form setting that affects data collection, analytics, and response deduplication.

Authentication Modes

ModeData CollectedDeduplicationUse Case
Fully AnonymousNone (no login required)Cookie-based sessionPublic surveys, feedback forms
Email OnlyEmail address (self-reported)Email uniqueness checkSurveys requiring follow-up
Verified EmailVerified email via magic linkVerified email uniquenessEmployee surveys, customer feedback
SSO RequiredAuthenticated user profileUser ID uniquenessInternal surveys, compliance forms
Invite OnlyPre-registered email listInvite token uniquenessPanel surveys, paid research

Session Management for Anonymous Responses

C# — Anonymous Response Handler
public class AnonymousResponseHandler
{
    public string GetOrCreateSessionId(
        HttpRequest request, HttpResponse response)
    {
        if (request.Cookies.TryGetValue("form_session", out var existing))
            return existing;

        var sessionId = Convert.ToBase64String(
            RandomNumberGenerator.GetBytes(16))
            .Replace("+", "-").Replace("/", "_").TrimEnd('=');

        response.Cookies.Append("form_session", sessionId,
            new CookieOptions
            {
                HttpOnly = true,
                Secure = true,
                SameSite = SameSitePolicy.Lax,
                Expires = DateTimeOffset.UtcNow.AddYears(1),
                Path = "/"
            });

        return sessionId;
    }

    public async Task<bool> IsDuplicateAsync(
        Guid formId, string sessionId, string? email = null)
    {
        var bySession = await _db.Responses.AnyAsync(r =>
            r.FormId == formId
            && r.SessionId == sessionId
            && r.Status == ResponseStatus.Completed);

        if (bySession) return true;

        if (!string.IsNullOrEmpty(email))
        {
            var byEmail = await _db.Responses.AnyAsync(r =>
                r.FormId == formId
                && r.Metadata["email"].ToString() == email
                && r.Status == ResponseStatus.Completed);
            if (byEmail) return true;
        }

        return false;
    }
}

17. Multi-Language Forms

Multi-language forms allow a single form to serve respondents in different languages. Translations are stored as locale-specific overrides on top of the base form schema.

Translation Model

C# — Translation Schema
public class FormTranslation
{
    public string Locale { get; set; } // "en", "es", "fr", "de", "ja"
    public string FormTitle { get; set; }
    public string FormDescription { get; set; }
    public Dictionary<string, QuestionTranslation> Questions { get; set; }
    public Dictionary<string, string> UIStrings { get; set; }
    // "submit_button": "Submit" -> "Enviar"
    // "required_field": "This field is required"
    //                    -> "Este campo es obligatorio"
    // "thank_you": "Thank you!" -> "!Gracias!"
}

public class QuestionTranslation
{
    public string Title { get; set; }
    public string Description { get; set; }
    public Dictionary<string, string> Options { get; set; }
    public string Placeholder { get; set; }
    public string ValidationMessage { get; set; }
}

Language Selection Strategy

  1. URL parameter: ?lang=es (highest priority)
  2. Browser's Accept-Language header
  3. Saved preference in cookie
  4. Form's default language

18. Quiz Mode & Auto-Grading

Quiz mode transforms a form into a scored assessment with correct answers, point values, time limits, and automatic grading. It is essential for educational platforms, training assessments, and certification tests.

Quiz Configuration

C# — Quiz Settings
public class QuizSettings
{
    public bool IsEnabled { get; set; }
    public int? TimeLimitMinutes { get; set; }
    public bool ShowCorrectAnswers { get; set; }
    public bool ShowScore { get; set; }
    public bool AllowRetakes { get; set; }
    public int? MaxRetakes { get; set; }
    public int? PassingScorePercentage { get; set; }
    public string PassingMessage { get; set; }
    public string FailingMessage { get; set; }
    public bool ShuffleQuestions { get; set; }
    public bool ShuffleOptions { get; set; }
    public int? QuestionsPerPage { get; set; }
}

public class QuestionQuizConfig
{
    public int Points { get; set; } = 1;
    public List<string> CorrectOptionIds { get; set; }
    public string CorrectTextAnswer { get; set; }
    public decimal? CorrectNumericAnswer { get; set; }
    public string Explanation { get; set; }
    public bool CaseSensitive { get; set; }
    public decimal? NumericTolerance { get; set; }
}

Grading Algorithm

C# — Auto-Grading Engine
public class GradingEngine : IGradingEngine
{
    public QuizResult GradeQuiz(
        FormSchema form, List<Answer> answers)
    {
        var result = new QuizResult();
        var answerMap = answers.ToDictionary(a => a.QuestionId);
        int totalPoints = 0;
        int earnedPoints = 0;

        foreach (var question in form.Questions
            .Where(q => q.QuizConfig != null))
        {
            totalPoints += question.QuizConfig.Points;

            if (!answerMap.TryGetValue(question.Id, out var answer))
            {
                result.Details.Add(new QuestionGrade
                {
                    QuestionId = question.Id,
                    IsCorrect = false,
                    PointsEarned = 0,
                    PointsPossible = question.QuizConfig.Points
                });
                continue;
            }

            var isCorrect = EvaluateAnswer(question, answer);
            var points = isCorrect ? question.QuizConfig.Points : 0;
            earnedPoints += points;

            result.Details.Add(new QuestionGrade
            {
                QuestionId = question.Id,
                IsCorrect = isCorrect,
                PointsEarned = points,
                PointsPossible = question.QuizConfig.Points,
                Explanation = question.QuizConfig.Explanation,
                CorrectAnswer = GetCorrectAnswerDisplay(question)
            });
        }

        result.TotalPoints = totalPoints;
        result.EarnedPoints = earnedPoints;
        result.Percentage = totalPoints > 0
            ? (double)earnedPoints / totalPoints * 100 : 0;
        result.Passed = form.QuizSettings.PassingScorePercentage.HasValue
            ? result.Percentage >= form.QuizSettings.PassingScorePercentage
            : true;

        return result;
    }

    private bool EvaluateAnswer(Question question, Answer answer)
    {
        var config = question.QuizConfig;

        return question.Config switch
        {
            MultipleChoiceConfig mc => answer.Value is ChoiceAnswer c
                && AreEqual(c.SelectedOptionIds, config.CorrectOptionIds),
            NumberConfig _ => answer.Value is NumberAnswer n
                && Math.Abs(n.Number - config.CorrectNumericAnswer.Value)
                    <= (config.NumericTolerance ?? 0.001m),
            TextConfig _ => answer.Value is TextAnswer t
                && string.Equals(t.Text.Trim(),
                    config.CorrectTextAnswer.Trim(),
                    config.CaseSensitive
                        ? StringComparison.Ordinal
                        : StringComparison.OrdinalIgnoreCase),
            _ => false
        };
    }
}

Timer Implementation

For timed quizzes, the timer runs client-side (for UX) but the deadline is enforced server-side. The server records the quiz start time when the form is first opened and calculates whether the submission arrived within the time limit. A clock-skew tolerance of 30 seconds prevents gaming.

19. Form Scheduling & Response Limits

Forms can be configured with availability windows and response caps to control when they accept submissions and how many responses they collect.

Scheduling Configuration

C# — Form Scheduling
public class FormSchedule
{
    public DateTime? OpensAt { get; set; }
    public DateTime? ClosesAt { get; set; }
    public int? MaxResponses { get; set; }
    public bool AllowPartialAfterClose { get; set; }
    public string ClosedMessage { get; set; }
    public string FullMessage { get; set; }
    public List<AvailabilityWindow> RecurringWindows { get; set; }
}

public class AvailabilityWindow
{
    public DayOfWeek Day { get; set; }
    public TimeOnly OpensAt { get; set; }
    public TimeOnly ClosesAt { get; set; }
}

public bool IsFormOpen(Form form, DateTime utcNow)
{
    var schedule = form.Settings.Schedule;

    if (schedule.OpensAt.HasValue && utcNow < schedule.OpensAt)
        return false;

    if (schedule.ClosesAt.HasValue && utcNow > schedule.ClosesAt)
        return false;

    if (schedule.RecurringWindows.Any())
    {
        var today = utcNow.DayOfWeek;
        var time = TimeOnly.FromDateTime(utcNow);
        var window = schedule.RecurringWindows
            .FirstOrDefault(w => w.Day == today);
        if (window == null) return false;
        if (time < window.OpensAt || time > window.ClosesAt)
            return false;
    }

    return true;
}

20. Partial Responses & Save-and-Continue

Long forms benefit from save-and-continue functionality. Respondents can save partial progress and resume later via a unique link sent to their email.

Partial Save Flow

sequenceDiagram participant R as Respondent participant API as Response API participant DB as Database participant Email as Email Service R->>API: PUT /forms/token/responses/sessionId/partial Note right of R: Auto-save on every answer change API->>DB: UPSERT partial response status=InProgress API-->>R: Saved 200 OK R->>API: POST /forms/token/responses/sessionId/save-and-send API->>DB: Update response with email API->>Email: Send resume link API-->>R: Check your email Note over R: Later on different device R->>API: GET /forms/token/resume/resumeToken API->>DB: Lookup response by resume token API-->>R: Form pre-filled with saved answers R->>API: POST responses/sessionId final submit API->>DB: Update status to Completed
C# — Partial Response Service
public class PartialResponseService
{
    private readonly AppDbContext _db;

    public async Task SavePartialAsync(
        string formToken, string sessionId, List<Answer> answers)
    {
        var form = await _db.Forms.FirstOrDefaultAsync(
            f => f.ShareToken == formToken);

        var response = await _db.Responses
            .FirstOrDefaultAsync(r =>
                r.FormId == form.Id
                && r.SessionId == sessionId);

        if (response == null)
        {
            response = new Response
            {
                FormId = form.Id,
                SessionId = sessionId,
                Status = ResponseStatus.InProgress,
                StartedAt = DateTime.UtcNow
            };
            _db.Responses.Add(response);
        }

        response.Answers = answers;
        response.UpdatedAt = DateTime.UtcNow;
        await _db.SaveChangesAsync();
    }

    public async Task<string> GenerateResumeLinkAsync(
        Guid responseId, string? email)
    {
        var resumeToken = Convert.ToBase64String(
            RandomNumberGenerator.GetBytes(32))
            .Replace("+", "-").Replace("/", "_").TrimEnd('=');

        var response = await _db.Responses.FindAsync(responseId);
        response.ResumeToken = resumeToken;
        response.ResumeEmail = email;
        await _db.SaveChangesAsync();

        return $"https://forms.yourapp.com/resume/{resumeToken}";
    }
}
Auto-Save Implementation: The client debounces saves (500ms after last change) and uses IndexedDB as a local backup. If the network is unavailable, changes queue locally and sync when connectivity resumes. This provides an offline-capable experience similar to Google Docs.

21. Notifications on Submission

Form owners need immediate notification when responses arrive. The system supports email alerts, Slack notifications, webhook triggers, and in-app notifications.

Notification Configuration

C# — Notification Settings
public class FormNotificationSettings
{
    public bool EmailOnResponse { get; set; } = true;
    public List<string> NotificationEmails { get; set; }
    public bool DailyDigest { get; set; }
    public bool WeeklyDigest { get; set; }
    public int DigestHour { get; set; } = 9;

    public SlackConfig Slack { get; set; }
    public WebhookConfig Webhook { get; set; }
    public bool InAppNotification { get; set; } = true;
}

public class SlackConfig
{
    public bool Enabled { get; set; }
    public string WebhookUrl { get; set; }
    public string Channel { get; set; }
    public string MessageTemplate { get; set; }
}

public class WebhookConfig
{
    public bool Enabled { get; set; }
    public string Url { get; set; }
    public string Secret { get; set; } // HMAC signing
    public List<string> Events { get; set; }
}

public class NotificationWorker : IEventHandler<ResponseSubmittedEvent>
{
    public async Task HandleAsync(ResponseSubmittedEvent evt)
    {
        var settings = await _settingsService.GetForFormAsync(evt.FormId);

        if (settings.EmailOnResponse)
            await _emailService.SendResponseAlertAsync(
                settings.NotificationEmails, evt);

        if (settings.Slack?.Enabled)
            await _slackService.SendAsync(settings.Slack.WebhookUrl, evt);

        if (settings.Webhook?.Enabled)
            await _webhookService.DispatchAsync(settings.Webhook, evt);
    }
}

22. Integrations — Slack, Zapier, Webhooks

Integrations connect the forms platform to the broader ecosystem, enabling automated workflows triggered by form submissions.

Integration Architecture

graph TB subgraph "Event Sources" RS[Response Submitted] RC[Response Completed] FC[Form Closed] end subgraph "Integration Router" ER[Event Router] HC[HMAC Signer] RR[Retry Handler] end subgraph "Destinations" WH[Webhooks] SL[Slack] ZP[Zapier] GS[Google Sheets] MA[Mailchimp] end RS --> ER RC --> ER FC --> ER ER --> HC HC --> RR RR --> WH RR --> SL RR --> ZP RR --> GS RR --> MA

Webhook Implementation

C# — Webhook Dispatcher
public class WebhookDispatcher
{
    private readonly HttpClient _httpClient;
    private readonly AppDbContext _db;

    public async Task DispatchAsync(
        WebhookConfig config, ResponseSubmittedEvent evt)
    {
        var payload = new WebhookPayload
        {
            Event = "response.submitted",
            Timestamp = DateTime.UtcNow,
            FormId = evt.FormId,
            ResponseId = evt.ResponseId,
            Data = evt.Answers
        };

        var json = JsonSerializer.Serialize(payload);
        var content = new StringContent(json, Encoding.UTF8, "application/json");

        var signature = ComputeHmacSha256(config.Secret, json);
        content.Headers.Add("X-Webhook-Signature", signature);
        content.Headers.Add("X-Webhook-Event", "response.submitted");

        for (int attempt = 0; attempt < 3; attempt++)
        {
            try
            {
                var response = await _httpClient.PostAsync(config.Url, content);
                if (response.IsSuccessStatusCode) return;

                await LogWebhookAttemptAsync(config.Id, evt, attempt,
                    response.StatusCode);
            }
            catch (HttpRequestException ex)
            {
                await LogWebhookAttemptAsync(config.Id, evt, attempt, ex);
            }

            await Task.Delay(TimeSpan.FromSeconds(Math.Pow(5, attempt)));
        }

        await MarkWebhookUnhealthyAsync(config.Id);
    }

    private string ComputeHmacSha256(string secret, string payload)
    {
        using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
        var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));
        return Convert.ToHexString(hash).ToLowerInvariant();
    }
}

Google Sheets Integration

The Google Sheets integration uses OAuth 2.0 to connect to the user's Google account. Once authorized, a background worker syncs new responses to the spreadsheet every 5 minutes, appending rows incrementally. The sheet can be auto-created on first connection with headers matching the form questions.

Zapier Integration

Zapier integration uses the REST hook pattern: users create a Zap, the platform registers a webhook URL, and new responses trigger the webhook with a standardized payload. The platform also provides a Zapier action for creating responses programmatically.

23. Form Performance Analytics

Beyond response-level analytics, form owners need to understand form performance — how many people view, start, and complete their forms.

Key Metrics

MetricFormulaTarget Insight
View CountUnique page loadsHow many people see the form
Start RateStarted / ViewedFirst impression effectiveness
Completion RateCompleted / StartedForm usability and length impact
Average Completion TimeAvg(submittedAt - startedAt)Expected respondent effort
Median Completion TimeMedian of aboveSkew-resistant time estimate
Drop-Off Rate per Question(N_q - N_q+1) / N_qWhich questions cause abandonment
Device SplitDesktop vs Mobile vs TabletResponsive design priorities
Geographic DistributionGeoIP of submissionsAudience demographics
Referrer AnalysisHTTP Referer headerWhich channels drive responses
Time-of-Day DistributionHourly histogramOptimal send times for email campaigns

Performance Tracking Implementation

C# — Performance Tracking Middleware
public class FormPerformanceMiddleware
{
    private readonly RequestDelegate _next;

    public async Task InvokeAsync(HttpContext context)
    {
        var path = context.Request.Path.Value;

        if (Regex.IsMatch(path,
            @"/api/v1/public/forms/([a-zA-Z0-9]+)$")
            && context.Request.Method == "GET")
        {
            var token = Regex.Match(path).Groups[1].Value;
            await TrackFormViewAsync(token, context);
        }

        await _next(context);
    }

    private async Task TrackFormViewAsync(
        string token, HttpContext context)
    {
        var formId = await _formService.GetFormIdByTokenAsync(token);

        var viewKey = $"perf:{formId}:views";
        await _redis.IncrementAsync(viewKey);

        var viewEvent = new FormViewEvent
        {
            FormId = formId,
            ViewedAt = DateTime.UtcNow,
            UserAgent = context.Request.Headers.UserAgent.ToString(),
            Referrer = context.Request.Headers.Referer.ToString(),
            IpAddress = context.Connection.RemoteIpAddress?.ToString(),
            DeviceType = DetectDeviceType(
                context.Request.Headers.UserAgent)
        };

        await _eventBus.PublishAsync(viewEvent);
    }
}

24. Spam Protection

Forms are a common vector for spam and abuse. The platform implements multiple layers of protection without creating friction for legitimate respondents.

Protection Layers

LayerMechanismThreshold
CAPTCHAGoogle reCAPTCHA v3 invisibleScore below 0.3 triggers challenge
Rate LimitingPer-IP per-form submission limits10 submissions per hour per IP
Duplicate DetectionSession cookie and timing heuristicsSame session within 30 seconds
Honeypot FieldsHidden fields that bots fillIf filled then reject
Token ExpirationForm link tokens with expiryConfigurable 7d 30d never
Bot DetectionBehavioral analysisSuspicious patterns then CAPTCHA
IP ReputationCheck against known bot IPsKnown bot then block
Content FilteringSpam keyword detectionMatch then flag for review
C# - Spam Protection Pipeline
public class SpamProtectionMiddleware
{
    private readonly IRedisCache _redis;

    public async Task<SpamCheckResult> CheckAsync(
        HttpRequest request, Guid formId)
    {
        var ip = request.HttpContext.Connection.RemoteIpAddress?.ToString();

        var rateLimitKey = $"ratelimit:{formId}:{ip}";
        var count = await _redis.IncrementAsync(rateLimitKey);
        if (count == 1)
            await _redis.SetExAsync(rateLimitKey,
                TimeSpan.FromHours(1), "1");

        if (count > 10)
            return new SpamCheckResult
                { Blocked = true, Reason = "Rate limit exceeded" };

        if (request.Form.ContainsKey("website_url"))
            return new SpamCheckResult
                { Blocked = true, Reason = "Honeypot triggered" };

        var captchaToken = request.Headers["X-Recaptcha-Token"].FirstOrDefault();
        if (!string.IsNullOrEmpty(captchaToken))
        {
            var score = await VerifyCaptchaAsync(captchaToken);
            if (score < 0.3)
                return new SpamCheckResult
                    { Blocked = true, Reason = "CAPTCHA score too low" };
        }

        return new SpamCheckResult { Blocked = false };
    }
}

25. Accessibility - WCAG Compliance

Form accessibility ensures all users, including those with disabilities, can interact with forms. WCAG 2.1 AA compliance is the target.

WCAG Requirements for Forms

WCAG CriterionRequirementImplementation
1.3.1 Info and RelationshipsForm fields have proper labelsHTML label elements associated via for/id
1.3.5 Identify Input PurposeInput purpose declaredautocomplete attributes on all fields
1.4.3 Contrast Minimum4.5:1 text contrast ratioTheme validator checks contrast ratios
1.4.11 Non-text Contrast3:1 for UI componentsButtons, borders, focus indicators
2.1.1 KeyboardAll functionality via keyboardTab order, Enter to select, Arrow keys
2.4.3 Focus OrderLogical focus sequenceVisual order matches DOM order
2.4.6 Headings and LabelsDescriptive labelsRequired field indicators, error descriptions
2.4.7 Focus VisibleVisible focus indicatorCustom focus ring CSS
3.3.1 Error IdentificationErrors described in textInline errors with aria-describedby
3.3.2 Labels or InstructionsLabels and help textEvery question has visible label and description
3.3.3 Error SuggestionSuggest correctionsFormat hints, regex patterns, examples
4.1.2 Name Role ValueARIA attributesCustom widgets use ARIA roles and states

Accessibility Testing

  • Automated: Axe-core integrated into CI pipeline, runs on every component render
  • Manual: Keyboard-only navigation testing, screen reader testing (NVDA, VoiceOver)
  • Contrast: Theme customizer warns when color combinations fail WCAG contrast ratios
  • ARIA Live Regions: Dynamic content updates announced via aria-live=polite

26. Mobile Optimization

Over 60% of form responses come from mobile devices. The mobile experience must be fast, touch-friendly, and fully functional.

Mobile-Specific Optimizations

OptimizationDetails
Responsive LayoutSingle-column on mobile, multi-column on desktop
Touch TargetsMinimum 44x44px for all interactive elements
Virtual KeyboardAppropriate input types for email, tel, number
Lazy LoadingQuestions load progressively not all at once
Offline SupportService worker caches form, responses queue locally
Image OptimizationWebP/AVIF with fallbacks, responsive srcset
Font LoadingFont-display swap for fast first paint
Minimized JavaScriptTree-shaken bundle, code-split by question type
Viewport Metawidth=device-width, initial-scale=1
PWA SupportInstallable as home screen app, offline capable

27. Monitoring and Observability

Comprehensive monitoring ensures the platform reliability and provides insights into usage patterns and performance.

Monitoring Stack

graph TB subgraph "Instrumentation" APP[Application Code] MW[Middleware] MET[Prometheus Metrics] LOG[Structured Logs] end subgraph "Collection" PROM[Prometheus] LOKI[Loki / ELK] JAEGER[Jaeger Distributed Tracing] end subgraph "Visualization and Alerting" GRAFANA[Grafana Dashboards] ALERT[AlertManager] PAGER[PagerDuty / OpsGenie] end APP --> MW MW --> LOG APP --> MET MET --> PROM LOG --> LOKI APP --> JAEGER PROM --> GRAFANA PROM --> ALERT ALERT --> PAGER

Key Metrics and Alerts

MetricThresholdSeverity
Response submission latency p99above 500msWarning
Response submission latency p99above 2sCritical
Form load time p95above 300msWarning
Error rate (5xx)above 0.1%Warning
Error rate (5xx)above 1%Critical
Database connection pool utilizationabove 80%Warning
Message queue lagabove 10000 messagesWarning
S3 upload failure rateabove 0.5%Critical
Analytics dashboard update lagabove 30 secondsWarning
CAPTCHA verification failure rateabove 5%Warning
Virus scan queue depthabove 1000Warning
Export job duration p95above 10 minutesWarning

Distributed Tracing

Every request gets a trace ID that follows it through all services. This is critical for debugging latency issues in the response submission path, which touches the API Gateway, Response Service, PostgreSQL, Redis, and message queue.

28. Security

Forms often collect sensitive data - personal information, employee feedback, medical questionnaires. Security is paramount.

Security Measures

LayerMeasureDetails
TransportTLS 1.3All traffic encrypted, HSTS enforced
StorageAES-256 encryption at restDatabase encryption, S3 SSE-KMS
AuthenticationJWT + refresh tokensShort-lived access tokens (15 min)
AuthorizationRBACOwner, Editor, Viewer roles per form
API SecurityRate limiting, input validationOWASP Top 10 mitigation
File SecurityVirus scanning, type validationClamAV, MIME type verification
XSS PreventionCSP, output encodingForm titles and descriptions sanitized
CSRF ProtectionDouble-submit cookie patternAll mutation endpoints protected
SQL InjectionParameterized queries (EF Core)No raw SQL concatenation
Data MaskingResponse data masked in logsPII never appears in application logs
Audit LoggingAll admin actions loggedForm edits, response deletes, sharing changes
Vulnerability ScanningDependency scanning (Snyk)Weekly automated scans
Penetration TestingAnnual third-party pentestScope includes form submission, file upload
Critical: Never log response contents in plaintext. If response data is logged for debugging, all PII fields must be redacted. Implement structured logging with PII field masking at the logging framework level.

29. Compliance - GDPR for Survey Data

Survey data often contains personal data subject to GDPR, CCPA, and other privacy regulations. The platform must support data subject rights and compliance requirements.

GDPR Requirements Implementation

GDPR RightImplementation
Right to Information (Art. 13-14)Form owners can add privacy notice and consent checkbox
Right of Access (Art. 15)API endpoint to export all data for a user
Right to Erasure (Art. 17)Response deletion cascade: delete answers, files, analytics
Right to Rectification (Art. 16)Response editing by respondent if enabled by form owner
Data Portability (Art. 20)Machine-readable export (JSON, CSV)
Consent ManagementConsent checkbox with timestamp, IP, and version tracking
Data Processing AgreementPlatform acts as processor; form owners are controllers
Data RetentionConfigurable retention periods, auto-deletion after expiry
Breach NotificationAutomated detection and 72-hour notification pipeline
C# - GDPR Data Deletion Service
public class GdprDeletionService
{
    private readonly AppDbContext _db;
    private readonly IBlobStorage _storage;
    private readonly IAnalyticsStore _analytics;

    public async Task DeleteAllUserDataAsync(Guid userId)
    {
        using var transaction = await _db.Database.BeginTransactionAsync();

        var responses = await _db.Responses
            .Where(r => r.UserId == userId)
            .ToListAsync();

        var responseIds = responses.Select(r => r.Id).ToList();

        await _db.Answers
            .Where(a => responseIds.Contains(a.ResponseId))
            .ExecuteDeleteAsync();

        var files = await _db.FileAttachments
            .Where(f => f.Answers.Any(a => responseIds.Contains(a.ResponseId)))
            .ToListAsync();

        foreach (var file in files)
            await _storage.DeleteAsync(file.StorageKey);

        await _db.FileAttachments
            .Where(f => f.Answers.Any(a => responseIds.Contains(a.ResponseId)))
            .ExecuteDeleteAsync();

        await _db.Responses
            .Where(r => r.UserId == userId)
            .ExecuteDeleteAsync();

        await _analytics.AnonymizeUserDataAsync(userId);

        await _db.Users.Where(u => u.Id == userId).ExecuteDeleteAsync();

        await transaction.CommitAsync();

        await _auditLog.LogAsync(new AuditEntry
        {
            Action = "GDPR_DELETION",
            TargetUserId = userId,
            Details = $"All data deleted. {responses.Count} responses, "
                      + $"{files.Count} files removed.",
            Timestamp = DateTime.UtcNow
        });
    }
}

30. Cost Estimation

Running a forms platform at scale involves predictable infrastructure costs. Here is an estimate for a platform handling 50M forms and 10B responses.

Monthly Cost Breakdown (AWS)

ComponentSpecificationMonthly Cost (USD)
PostgreSQL (RDS Multi-AZ)db.r6g.2xlarge x 3, 5TB storage,500
Redis (ElastiCache)r6g.xlarge cluster, 3 shards,400
S3 Storage10TB file uploads + 2TB analytics exports
S3 Requests100M PUT + 500M GET per month
CloudFront CDN500TB transfer, 1B requests,000
EKS (Kubernetes)20 x m6i.2xlarge nodes,500
Application Load Balancer2 ALBs with high throughput
Kafka (MSK)m5.large x 3 brokers,200
ClickHouse (self-managed)r6i.2xlarge x 4 nodes, 4TB SSD,200
OpenSearchr6g.xlarge.search x 3 nodes,800
Email (SES)10M emails/month
DNS (Route 53)50 hosted zones, 500M queries
Monitoring (CloudWatch + Grafana)Custom metrics, dashboards
WAFWeb ACL + managed rules
Data TransferInter-AZ, internet egress,000
Total~,700/month
Cost Optimization: Reserved instances (1-year commitment) reduce compute costs by ~30%. Spot instances for batch workers (export, analytics) save another 60% on those workloads. S3 Intelligent-Tiering reduces storage costs for infrequently accessed files.

31. Testing Strategy

A comprehensive testing strategy ensures correctness across the dynamic schema validation, branching logic, real-time analytics, and integration systems.

Testing Pyramid

LevelScopeCount TargetKey Areas
Unit TestsIndividual functions and classes2000+Schema validation, grading engine, logic engine, theme merging
Integration TestsService and database interactions500+API endpoints, response persistence, analytics aggregation
Contract TestsAPI contracts between services200+Event schemas, webhook payloads, API request/response
E2E TestsFull user workflows100+Form creation, submission, analytics, export
Performance TestsLoad and stress testing20+Concurrent submissions, large form rendering, export throughput
Security TestsVulnerability assessment50+SQL injection, XSS, CSRF, file upload abuse
Accessibility TestsWCAG compliance30+Axe-core scans, keyboard navigation, screen reader

Key Test Scenario: Branching Logic

C# - Branching Logic Unit Test
[Fact]
public async Task SkipLogic_ShouldSkipQuestions_WhenConditionMet()
{
    var form = new FormSchema
    {
        Questions = new List<Question>
        {
            new MultipleChoiceConfig
            {
                Id = "q1",
                Title = "Do you have a car?",
                Options = new List<OptionItem>
                {
                    new() { Id = "yes", Label = "Yes" },
                    new() { Id = "no", Label = "No" }
                }
            },
            new TextConfig { Id = "q2", Title = "What brand?" },
            new RatingConfig { Id = "q3", Title = "How satisfied?" },
            new TextConfig { Id = "q4", Title = "Why not satisfied?" }
        },
        LogicRules = new List<LogicRule>
        {
            new()
            {
                SourceQuestionId = "q1",
                Condition = new EqualsCondition
                    { QuestionId = "q1", OptionId = "no" },
                Action = new SkipToAction { TargetQuestionId = "q3" },
                Priority = 1
            }
        }
    };

    var answers = new List<Answer>
    {
        new() { QuestionId = "q1", Value = new ChoiceAnswer
            { SelectedOptionIds = new() { "no" } } },
        new() { QuestionId = "q3", Value = new RatingAnswer { Rating = 4 } },
        new() { QuestionId = "q4", Value = new TextAnswer
            { Text = "Too expensive" } }
    };

    var engine = new LogicEngine();
    var processedAnswers = engine.ProcessAnswers(form, answers);

    Assert.Equal(2, processedAnswers.Count);
    Assert.DoesNotContain(processedAnswers,
        a => a.QuestionId == "q2");
    Assert.Contains(processedAnswers,
        a => a.QuestionId == "q3");
}

Load Testing

Use k6 or Locust to simulate 1M concurrent respondents filling out forms. Key scenarios: (1) form rendering under cache miss conditions, (2) response submission spike during a viral form campaign, (3) simultaneous export of large response sets, (4) real-time analytics update throughput under load.

32. Interview Q&A

Q1: How would you handle a form with 10,000 questions?

A: Paginate the form into sections. Load questions lazily - only the current section and adjacent sections are in the DOM. Use virtual scrolling for the builder UI. Store the form schema as a single JSON document but render questions on-demand. The response submission endpoint accepts answers per-section, not all at once.

Q2: How do you prevent a form owner from seeing responses that contain PII of other users?

A: The form owner has access to all responses for forms they own. However, if the form collects data on behalf of a third party (e.g., employee survey by HR), implement field-level access control. Specific PII fields can be encrypted with keys only the respondent holds, making them visible only to the respondent themselves.

Q3: How would you design the branching logic evaluation to be O(n) regardless of rule count?

A: On form publish, precompute a decision tree / skip graph. Group rules by source question ID into a dictionary. At evaluation time, process questions in order and only look up rules for the current question (O(1) lookup). The overall evaluation is O(number of questions), not O(number of rules).

Q4: How do you handle schema evolution when a form is updated while responses are being collected?

A: Each form has a version number. Every response stores which form version it was submitted against. When displaying analytics, responses from different versions are handled correctly - questions that existed in v1 but not v2 show N/A for v2 responses. The schema validator uses the version-specific schema at submission time.

Q5: How would you support real-time collaborative form editing?

A: Use Operational Transformation (OT) or CRDT-based collaboration. Each block has a unique ID, and operations (insert, move, update, delete) are expressed as transformations on the block list. A WebSocket connection broadcasts operations to all connected editors. Server-side conflict resolution merges concurrent edits.

Q6: How would you handle a form that goes viral and receives 100,000 responses per minute?

A: Auto-scale the response ingestion service horizontally. Use a message queue (Kafka) to decouple submission from processing. Implement backpressure: if the queue is full, respondents see a "try again shortly" message rather than dropping requests. Batch writes to the database (micro-batching within 100ms windows). Analytics counters in Redis update immediately even if the database write is delayed.

Q7: How do you ensure form rendering is fast globally?

A: CDN-cache the rendered form HTML at edge locations. Form schemas are versioned, so the CDN can cache indefinitely (cache invalidation via version number in URL). Static assets (CSS, JS, images) use content-hashed filenames for aggressive caching. The form HTML payload is typically under 50KB, well within CDN optimal range.

Q8: How would you implement quiz auto-grading for essay-type (long text) questions?

A: Essay grading requires human review or NLP-based evaluation. For NLP, use a rubric-based scoring model: define keywords, phrases, and semantic similarity thresholds against a reference answer. Use embeddings (e.g., OpenAI ada-002 or sentence-transformers) to compute cosine similarity between the respondent's answer and the reference. The score is a confidence-weighted combination of keyword match and semantic similarity.

Q9: How would you design the form preview to accurately simulate branching logic?

A: The preview mode runs the logic engine client-side using the same rule set as production. As the builder user navigates the preview, each answer change triggers a re-evaluation of the logic graph, hiding/showing questions dynamically. This provides an accurate WYSIWYG experience without hitting the server.

Q10: How would you handle file uploads when the respondent has a slow or unreliable connection?

A: Use chunked upload with resumability. The client splits the file into 5MB chunks and uploads them in parallel using presigned URLs. Each chunk is tracked server-side. If the connection drops, the upload resumes from the last successful chunk. TUS (resumable upload protocol) is a good standard to follow. The form can be submitted with partial uploads - remaining chunks continue uploading in the background.

33. Summary and Key Takeaways

graph TB subgraph "Core Pillars" P1[Form Builder UX] P2[Dynamic Schema] P3[Response Scale] P4[Real-Time Analytics] P5[Integrations] end subgraph "Critical Decisions" D1[JSON Schema for flexible questions] D2[Redis + ClickHouse for analytics] D3[CDN for global form delivery] D4[Async processing for notifications] D5[Partitioned responses for scale] end P1 --> D1 P2 --> D1 P3 --> D5 P4 --> D2 P5 --> D4 P3 --> D3

Key Architectural Decisions

DecisionChoiceTrade-off
Form Schema StorageJSONB in PostgreSQLFlexible but harder to query than relational
Analytics EngineRedis (real-time) + ClickHouse (historical)Two systems to maintain, but each optimized for its use case
Response PartitioningHash by form_id, then by dateGood query locality but rebalancing needed as forms grow
File UploadsPresigned URLs to S3Direct upload reduces server load but requires client-side validation
Form DeliveryCDN-cached static HTMLFast globally but requires version-based cache invalidation
Notification ProcessingAsync via message queueDecoupled but introduces eventual consistency
Logic EvaluationPrecomputed skip graphFast at runtime but adds complexity to form publish pipeline
Collaborative EditingOT/CRDT on block listComplex implementation but enables real-time collaboration

Scaling Milestones

MilestoneFormsResponsesArchitecture Adjustment
Launch (0-1M)100K10MSingle PostgreSQL, single app server, Redis for caching
Growth (1M-100M)10M1BAdd read replicas, CDN, message queue, ClickHouse for analytics
Scale (100M+)100M+10B+Partitioned databases, Kubernetes auto-scaling, global CDN, multi-region
Enterprise1B+100B+Multi-region active-active, custom CDN nodes, dedicated analytics cluster

Final Recommendations

  1. Start with a strong data model: The dynamic schema approach (JSONB for questions and answers) is the foundation. Get this right first.
  2. Separate read and write paths: Form rendering (read) and response submission (write) scale independently. Optimize each path separately.
  3. Build the analytics pipeline early: Real-time analytics is a key differentiator. Redis for counters, ClickHouse for OLAP queries, WebSockets for live updates.
  4. Design for offline-first: The builder should auto-save, and the respondent experience should survive network interruptions.
  5. Compliance from day one: GDPR and privacy compliance are hard to retrofit. Build data deletion, consent tracking, and audit logging into the core.
  6. Test branching logic thoroughly: Conditional logic is the most complex feature. Property-based testing and exhaustive scenario coverage are essential.
  7. Invest in the file upload pipeline: Presigned URLs, virus scanning, and resumable uploads are table stakes for an enterprise forms platform.
  8. Monitor everything: Form submission latency, analytics freshness, export job duration, webhook delivery success rates - all need alerts.
Bottom Line: A survey and forms builder is a deceptively complex system that combines a rich client-side builder experience with a high-throughput write path for response collection, real-time analytics, and a broad integration ecosystem. The key to success is a flexible data model, clear separation of concerns between services, and a strong focus on both respondent experience and form owner insights.

Survey & Forms Builder Platform - Senior+ System Design Guide | Ayodhyya