Form/Question/TextQuestion/ChoiceQuestion/Response classes, per-question-type validation via polymorphism, and Composite pattern for nested/conditional sections.
Published September 23, 2026
abstract class Question {
String id;
String prompt;
boolean required;
abstract ValidationResult validate(Answer answer); // polymorphism — no question-type switch anywhere else
}
class TextQuestion extends Question {
Integer maxLength;
ValidationResult validate(Answer answer) {
String text = answer.getTextValue();
if (required && (text == null || text.isBlank())) return ValidationResult.invalid("Required field");
if (maxLength != null && text.length() > maxLength) return ValidationResult.invalid("Exceeds max length");
return ValidationResult.valid();
}
}
class ChoiceQuestion extends Question {
List<String> options;
boolean allowMultiple;
ValidationResult validate(Answer answer) {
List<String> selected = answer.getSelectedOptions();
if (required && selected.isEmpty()) return ValidationResult.invalid("Required field");
if (!allowMultiple && selected.size() > 1) return ValidationResult.invalid("Only one option allowed");
if (!options.containsAll(selected)) return ValidationResult.invalid("Invalid option selected");
return ValidationResult.valid();
}
}
class Form { List<Question> questions; }
class Response { Map<String, Answer> answersByQuestionId; }
Same OCP argument made throughout this course (Chess Engine Design's piece-movement polymorphism, Strategy Pattern's if/else-chain comparison): Form.validate(Response) never branches on question type — it iterates questions and calls each one's own validate(). Adding a new question type (a DateQuestion, a NumericRangeQuestion) means writing one new Question subclass with its own validate() implementation, with zero changes to Form or any existing question type's code.
abstract class FormElement { // the Composite abstraction — both Question and Section implement it
abstract ValidationResult validate(Response response);
abstract boolean isVisible(Response response); // supports conditional logic
}
class Section extends FormElement {
List<FormElement> children; // can hold Questions OR nested Sections
String showIfQuestionId; // conditional visibility: only show this section if a specific prior answer matches
String showIfValue;
boolean isVisible(Response response) {
if (showIfQuestionId == null) return true; // always visible
Answer trigger = response.getAnswer(showIfQuestionId);
return trigger != null && trigger.matches(showIfValue);
}
ValidationResult validate(Response response) {
if (!isVisible(response)) return ValidationResult.valid(); // hidden sections skip validation entirely
return children.stream()
.map(child -> child.validate(response))
.filter(ValidationResult::isInvalid)
.findFirst()
.orElse(ValidationResult.valid());
}
}
Modeling Question and Section as both implementing a shared FormElement abstraction is Composite (see Composite & Proxy) applied directly — a Section can contain a mix of individual Questions and further nested Sections, and validate() recurses through this tree uniformly, exactly like Directory.size() recursing through nested directories in the Composite & Proxy file-system example. The conditional-visibility check (showIfQuestionId/showIfValue) is what makes this genuinely useful for a real survey builder — an entire section of follow-up questions that only applies based on an earlier answer, with hidden sections correctly skipped during validation rather than incorrectly required.
Q: Why does Section.validate() short-circuit on the first invalid child (findFirst()) rather than collecting every validation error? A: A real form-builder UI typically wants ALL validation errors at once (so a user sees every problem, not just the first, before resubmitting) — this simplified version is a reasonable LLD-scope shortcut; a production version would likely collect a List<ValidationResult> across all children rather than stopping at the first failure, worth naming as a deliberate simplification rather than an oversight.
Q: How would conditional visibility handle a chain of dependencies (Section C only shows if Section B is visible AND a specific answer in B matches)? A: The current showIfQuestionId design only supports a single flat condition — a more general design might need isVisible() to accept an arbitrary predicate/expression over the full Response, or explicitly check the triggering section's own visibility first before evaluating its own condition, since a hidden section's 'answer' shouldn't be able to trigger a dependent section's visibility.
Q: Is Question really the right thing to make abstract, or should validate() logic live in a separate QuestionValidator class per type instead? A: Both are defensible — putting validate() directly on Question (as shown) keeps a question type's data and its validation rule colocated; a separate QuestionValidator hierarchy would more closely mirror Strategy pattern's separation-of-algorithm-from-data philosophy, useful specifically if the SAME question type needed different validation rules in different contexts (a length limit that varies per form, not per question type).
Q: How would you extend ChoiceQuestion validation to support 'exactly N selections required' rather than just required/optional? A: A straightforward extension — add a minSelections/maxSelections range on ChoiceQuestion (generalizing the current binary allowMultiple flag) and check selected.size() against that range in validate(), no structural change needed, just additional fields and a slightly richer check within the same method.