Machine Coding Problem

Survey Monkey (Survey System)

maco60macoAllproductivitydynamic-form-modeling
Commonly Asked By:QualtricsGoogleMicrosoft

Requirements & System Scope

Functional Scope (In-Scope)

  • Dynamic Form Schemas: Create surveys composed of diverse question types (Choice, Text, Scale).
  • Skip-Logic Rule Routing: Define skip rules (e.g., if Q1 is 'No', skip to Q5) to navigate forms dynamically.
  • Strict Path Validation: Verify that submitted survey responses follow a valid logical path on form completion.
  • Analytics Aggregation: Aggregate answers by question type (choice percentages, mean rating scores) using the Visitor Design Pattern.

Explicit Boundaries (Out-of-Scope)

  • No Third-Party Email Dispatchers: Excludes direct integration with Mailchimp or transactional SMTP client configurations.
  • No Machine Learning Sentiment Extractors: Bypasses automatic NLP sentiment analysis for text-based answers.

Class Diagram & Entity Relationships

Structural layout showing relationships between surveys, polymorphic question types, and skip rules:

Loading...
  • Polymorphic Question Node: Question abstracts dynamic layouts and validates answers according to schema parameters.
  • Conditional Rules: SkipRule stores condition checks and target question jumps.

Design Patterns & SOLID Principles

  • Visitor Pattern (Result Aggregators): Using visitors to aggregate results avoids adding analytics code to Question classes, satisfying the Open-Closed Principle.
  • Single Responsibility Principle (SRP):Question manages dynamic layouts; SkipRule handles logical routing; ResultAggregator calculates analytics.

Core Execution Workflows

Interactive Skip Logic Routing

  1. Renter answers current question: submitAnswer(surveyId, responseId, currentQuestionId, val).
  2. Query active skip rules for the current question:
    1. If a matching skip condition is met, return the target question ID.
    2. If no skip rule matches, return the next sequential question ID in the survey.
  3. Update client-side navigation paths.
  4. Upon final submission, cross-reference the path of completed answers against skip-logic rules to ensure integrity.

Concurrency & Thread Safety Strategy

When thousands of users submit survey responses in parallel:

  • Stateless Routing Checkers: Keep routing calculations completely stateless to allow safe parallel validation.
  • Thread-Safe Operations: Use concurrency lists (`CopyOnWriteArrayList` and `ConcurrentHashMap`) inside surveys to avoid errors when dynamic surveys are modified during active sessions.

Complete Clean Code Blueprint

Clean reference designs demonstrating dynamic skip logic in Java and Python:

// โ”€โ”€โ”€ JAVA BLUEPRINT โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
import java.util.*;
import java.util.concurrent.*;

enum QuestionType { CHOICE, SCALE, TEXT }

// Visitor Pattern for dynamic analytics aggregation
interface QuestionVisitor {
    void visit(ChoiceQuestion q);
    void visit(ScaleQuestion q);
    void visit(TextQuestion q);
}

abstract class Question {
    private final String id;
    private final String text;
    private final QuestionType type;

    public Question(String id, String text, QuestionType type) {
        this.id = id;
        this.text = text;
        this.type = type;
    }

    public String getId() { return id; }
    public String getText() { return text; }
    public QuestionType getType() { return type; }

    public abstract boolean validateResponse(Object answer);
    public abstract void accept(QuestionVisitor visitor);
}

class ChoiceQuestion extends Question {
    private final List<String> options;

    public ChoiceQuestion(String id, String text, List<String> options) {
        super(id, text, QuestionType.CHOICE);
        this.options = new ArrayList<>(options);
    }

    public List<String> getOptions() { return options; }

    @Override
    public boolean validateResponse(Object answer) {
        if (!(answer instanceof String)) return false;
        return options.contains(answer);
    }

    @Override
    public void accept(QuestionVisitor visitor) {
        visitor.visit(this);
    }
}

class ScaleQuestion extends Question {
    private final int minVal;
    private final int maxVal;

    public ScaleQuestion(String id, String text, int minVal, int maxVal) {
        super(id, text, QuestionType.SCALE);
        this.minVal = minVal;
        this.maxVal = maxVal;
    }

    public int getMinVal() { return minVal; }
    public int getMaxVal() { return maxVal; }

    @Override
    public boolean validateResponse(Object answer) {
        if (!(answer instanceof Integer)) return false;
        int val = (Integer) answer;
        return val >= minVal && val <= maxVal;
    }

    @Override
    public void accept(QuestionVisitor visitor) {
        visitor.visit(this);
    }
}

class TextQuestion extends Question {
    public TextQuestion(String id, String text) {
        super(id, text, QuestionType.TEXT);
    }

    @Override
    public boolean validateResponse(Object answer) {
        if (!(answer instanceof String)) return false;
        return !((String) answer).trim().isEmpty();
    }

    @Override
    public void accept(QuestionVisitor visitor) {
        visitor.visit(this);
    }
}

class SkipRule {
    private final String expectedAnswer;
    private final String jumpToQuestionId;

    public SkipRule(String expectedAnswer, String jumpToQuestionId) {
        this.expectedAnswer = expectedAnswer;
        this.jumpToQuestionId = jumpToQuestionId;
    }

    public String getExpectedAnswer() { return expectedAnswer; }
    public String getJumpToQuestionId() { return jumpToQuestionId; }
}

class SurveyResponse {
    private final String responseId;
    private final String surveyId;
    private final Map<String, Object> answers = new ConcurrentHashMap<>();

    public SurveyResponse(String responseId, String surveyId) {
        this.responseId = responseId;
        this.surveyId = surveyId;
    }

    public String getResponseId() { return responseId; }
    public String getSurveyId() { return surveyId; }
    public void putAnswer(String qId, Object answer) { answers.put(qId, answer); }
    public Object getAnswer(String qId) { return answers.get(qId); }
    public Map<String, Object> getAnswers() { return answers; }
}

class Survey {
    private final String id;
    private final List<Question> questions = new CopyOnWriteArrayList<>();
    private final Map<String, List<SkipRule>> skipRules = new ConcurrentHashMap<>();

    public Survey(String id) {
        this.id = id;
    }

    public String getId() { return id; }

    public void addQuestion(Question q) {
        questions.add(q);
    }

    public void addSkipRule(String questionId, String answer, String jumpToId) {
        skipRules.computeIfAbsent(questionId, k -> new CopyOnWriteArrayList<>())
                 .add(new SkipRule(answer, jumpToId));
    }

    public Question getQuestion(String qId) {
        for (Question q : questions) {
            if (q.getId().equals(qId)) return q;
        }
        return null;
    }

    public String getNextQuestionId(String currentId, Object answer) {
        // Evaluate skip-rules
        List<SkipRule> rules = skipRules.get(currentId);
        if (rules != null && answer != null) {
            for (SkipRule r : rules) {
                if (r.getExpectedAnswer().equalsIgnoreCase(answer.toString())) {
                    return r.getJumpToQuestionId();
                }
            }
        }

        // Sequential fallback
        for (int i = 0; i < questions.size() - 1; i++) {
            if (questions.get(i).getId().equals(currentId)) {
                return questions.get(i + 1).getId();
            }
        }
        return null; // Terminate survey
    }

    public List<Question> getQuestions() { return questions; }
}

class SurveyResponseValidator {
    public boolean validate(Survey survey, SurveyResponse response) {
        if (survey.getQuestions().isEmpty()) return true;

        String currentId = survey.getQuestions().get(0).getId();
        Set<String> expectedPath = new HashSet<>();

        while (currentId != null) {
            expectedPath.add(currentId);
            Question q = survey.getQuestion(currentId);
            Object ans = response.getAnswer(currentId);

            if (ans == null) {
                System.out.println("Validation fail: Missing answer on active logical path for question " + currentId);
                return false;
            }

            if (!q.validateResponse(ans)) {
                System.out.println("Validation fail: Invalid format value for question " + currentId + ": " + ans);
                return false;
            }

            currentId = survey.getNextQuestionId(currentId, ans);
        }

        // Check if any extra answers exist that were supposed to be skipped
        for (String qId : response.getAnswers().keySet()) {
            if (!expectedPath.contains(qId)) {
                System.out.println("Validation warning: Extra answer registered for skipped question " + qId);
                return false;
            }
        }

        return true;
    }
}

// Analytical Visitor implementation
class AnalyticsAggregator implements QuestionVisitor {
    private final List<SurveyResponse> responses;
    
    public AnalyticsAggregator(List<SurveyResponse> responses) {
        this.responses = responses;
    }

    @Override
    public void visit(ChoiceQuestion q) {
        Map<String, Integer> counts = new HashMap<>();
        for (SurveyResponse res : responses) {
            Object ans = res.getAnswer(q.getId());
            if (ans != null) {
                counts.put(ans.toString(), counts.getOrDefault(ans.toString(), 0) + 1);
            }
        }
        System.out.println("Choice Question [" + q.getText() + "] Results: " + counts);
    }

    @Override
    public void visit(ScaleQuestion q) {
        double total = 0.0;
        int count = 0;
        for (SurveyResponse res : responses) {
            Object ans = res.getAnswer(q.getId());
            if (ans instanceof Integer) {
                total += (Integer) ans;
                count++;
            }
        }
        double avg = count == 0 ? 0.0 : total / count;
        System.out.println("Scale Question [" + q.getText() + "] Average Score: " + String.format("%.2f", avg));
    }

    @Override
    public void visit(TextQuestion q) {
        List<String> entries = new ArrayList<>();
        for (SurveyResponse res : responses) {
            Object ans = res.getAnswer(q.getId());
            if (ans != null) {
                entries.add(ans.toString());
            }
        }
        System.out.println("Text Question [" + q.getText() + "] Responses: " + entries);
    }
}

// Complete Simulation
public class Main {
    public static void main(String[] args) {
        Survey survey = new Survey("S-101");

        // Add Questions
        survey.addQuestion(new ChoiceQuestion("Q-1", "Do you own a car?", Arrays.asList("Yes", "No")));
        survey.addQuestion(new TextQuestion("Q-2", "What brand of car is it?"));
        survey.addQuestion(new ScaleQuestion("Q-3", "Rate your car satisfaction", 1, 5));

        // Skip logic: If Q-1 answer is "No", jump straight to Q-3 (skip Q-2)
        survey.addSkipRule("Q-1", "No", "Q-3");

        // 1. Create a valid "Yes" path response
        SurveyResponse res1 = new SurveyResponse("R-1", "S-101");
        res1.putAnswer("Q-1", "Yes");
        res1.putAnswer("Q-2", "Tesla Model 3");
        res1.putAnswer("Q-3", 5);

        // 2. Create a valid "No" path response (skipped Q-2)
        SurveyResponse res2 = new SurveyResponse("R-2", "S-101");
        res2.putAnswer("Q-1", "No");
        res2.putAnswer("Q-3", 4);

        // 3. Create an invalid response (submitted Q-2 answer but chose "No")
        SurveyResponse res3 = new SurveyResponse("R-3", "S-101");
        res3.putAnswer("Q-1", "No");
        res3.putAnswer("Q-2", "Toyota");
        res3.putAnswer("Q-3", 3);

        SurveyResponseValidator validator = new SurveyResponseValidator();
        System.out.println("Is response 1 valid? " + validator.validate(survey, res1));
        System.out.println("Is response 2 valid? " + validator.validate(survey, res2));
        System.out.println("Is response 3 valid? " + validator.validate(survey, res3));

        // 4. Run visitor analytics
        System.out.println("\n--- Aggregating Analytics ---");
        List<SurveyResponse> allResponses = Arrays.asList(res1, res2);
        AnalyticsAggregator aggregator = new AnalyticsAggregator(allResponses);
        
        for (Question q : survey.getQuestions()) {
            q.accept(aggregator);
        }
    }
}

๐Ÿ’ฌReview

Help Us Improve

How helpful was this walkthrough?

Click a star to rate. We actively use this feedback to refine and update our system design content.

Placeholder
Optional but highly appreciated!

Discussion

Share your thoughts, ask questions, or help others.

Loading comments...