Form System Architecture

Overview

The Form System provides a question-based data capture mechanism built on the process entity model. It drives multi-step registration workflows for memberships, events, and other enrolment processes.

The core abstraction is:

  • A Form is a ProcessDefinition instantiated as a ProcessInstance

  • Each question in the form is a ProcessStep mapped to a FormField class

  • A FormController orchestrates the runtime lifecycle: person selection, question iteration, validation, and completion

This architecture separates the what (process configuration in the database) from the how (FormController and FormField classes in code), enabling the same code to drive different forms with different questions.

Process Entity Model

The form system is built on five core entities:

ProcessDefinition (workflow template)
├── type: "E" (Event), "M" (Membership), "T" (Test)
└── steps: List<ProcessStep> (ordered by seq)
    └── options: Set<ProcessStepOption> (choices)

ProcessInstance (runtime execution)
├── identifier: UUID (unique session key)
├── status: ProcessState (INIT → RUNNING → DONE)
├── definition: → ProcessDefinition
├── currentStep: → ProcessStep
├── startedBy: → OrgUser
├── people: Set<User> (persons in the process)
└── data: Set<ProcessData> (all answers)
    ├── key: String (field key, typically "DEFAULT")
    ├── value: String (answer)
    ├── step: → ProcessStep (optional)
    └── person: → User (optional)

ProcessInstance organises its data in a four-tier nested map for efficient lookup:

Step Person Scope

null

null

General process data

set

null

Step-specific data

set

set

Step and person-specific data (most common)

null

set

Person-specific process data

FormController Architecture

FormController is the base class that wraps a ProcessInstance and manages the form lifecycle. It is instantiated fresh for each form interaction (not long-lived).

Class Hierarchy

FormController (abstract base)
├── MembershipFormController  — membership enrolment with family logic
├── EventFormController       — event registration
└── TestFormController        — single-field testing (dev/test only)

Key Responsibilities

  • FormField registration — Each subclass registers its supported FormField types in a formFieldCandidates map (step code → class)

  • Person management — Manages the set of persons being processed, with add/remove capabilities

  • Step iterationsetNextStep() iterates through all steps from the beginning, stopping at the first step with invalid data

  • Validation orchestration — Coordinates validation across all persons for the current step

  • Data persistence — Manages ProcessData records for form answers

Factory Method

FormService.createFormController() is the factory that creates the appropriate controller based on ProcessDefinition.type:

private FormController createFormController(ProcessInstance instance) {
    String type = instance.getDefinition().getType();
    if (ProcessDefinition.EVENT_TYPE.equals(type)) {
        return new EventFormController(...);
    } else if (ProcessDefinition.MEMBERSHIP_TYPE.equals(type)) {
        return new MembershipFormController(...);
    }
    // Future: ProcessDefinition.TEST_TYPE for TestFormController
}

Lifecycle

INIT / RESUME
  │
  ├─ Person selection phase
  │   └─ Add/remove persons (candidates)
  │
  ▼
RUNNING
  │
  ├─ For each ProcessStep (in sequence order):
  │   ├─ Instantiate FormField from step.type code
  │   ├─ For each person:
  │   │   ├─ validateSavedFormData() — check persisted data
  │   │   └─ If invalid: STOP here, show this question
  │   └─ If all persons valid: continue to next step
  │
  ▼
DONE
  └─ All steps validated → onDone() hook

Validation Flow

When the front-end submits an answer via POST /api/forms/next:

  1. Apply answers — Map submitted data from FormDTO to FormPerson objects

  2. Validate submitted dataFormField.validateSubmittedData() checks the answer string

  3. Save form dataFormField.saveFormData() creates/updates ProcessData records

  4. AdvancesetNextStep() re-validates from the beginning, advancing past completed steps

  5. Return — Next incomplete step as FormDTO, or DONE status

Lifecycle Hooks

Subclasses can override these hooks to add domain-specific behaviour:

Hook Purpose

onBeforeProcessing()

Pre-processing before step iteration (e.g., assign membership criteria)

onDone()

Completion logic (e.g., create orders, memberships, event participants)

onValid()

Per-field hook called when a field passes validation

mustProcessPerson()

Per-field/person hook to skip a person for a specific step

validateAll()

Cross-field/person validation after individual validation passes

collectSummaryData()

Gather completion summary data for display

FormField Types

FormField is the abstract base class that wraps a ProcessStep to provide validation and data management for a specific question type. All implementations follow the constructor signature (FormController, ProcessStep) and are instantiated via reflection.

Step Code Reference

Code Class Description

TXT

FreeTextFormField

Free text input with optional length and regex pattern validation

SEL

DropDownOptionFormField

Dropdown/select populated from ProcessStep options

RBO

RadioButtonOptionFormField

Radio button selection with multiple options

ONE

RadioButtonPickOneFormField

Single radio button — select one option across all persons

BCB

CheckBoxBooleanFormField

Boolean checkbox (true/false)

ITC

IndemnityTermsConditionsFormField

Terms and conditions acceptance checkbox

CLV

CustomListValueFormField

Dropdown populated from pipe-delimited list in ProcessStep.listOptions

CON

CommunicationPreferenceFormField

Three-way toggle (Email / SMS / WhatsApp) with conditional contact input

EMC

EmergencyContactFormField

Composite field for emergency contact name and phone number

PHN

PhoneNumberFormField

Phone number with country code dropdown

CSI

CsaIdentificationFormField

CSA and UCI identification number capture

CSM

CsaMembershipCheckFormField

CSA membership status check via external API

CSL

CsaLicenseCheckFormField

CSA license status check via external API

ECA

EventCategorySelectFormField

Event category selection filtered by age and gender eligibility

ERT

EventRaceTypeSelectFormField

Event race type multi-select

MFM

MembershipMainSelectFormField

Family membership main member designation

MFA

MembershipAdultSelectFormField

Family membership adult selection

MFC

MembershipChildSelectFormField

Family membership child selection

MCS

MembershipCrossSellFormField

Cross-sell membership selection

Registration by Controller

FormField types are registered in the formFieldCandidates map during construction:

  • FormController (base) — TXT, SEL, RBO, ONE, BCB, ITC, CLV, CON, EMC, PHN, CSI, CSM, CSL

  • EventFormController — adds ECA, ERT

  • MembershipFormController — adds MFM, MFA, MFC, MCS

  • TestFormController — registers all types (union of all controllers)

REST API

The form system is exposed through FormResource at /api/forms:

Method Endpoint Description

GET

/api/forms/membership

Start or resume a membership form session

POST

/api/forms/next

Submit current step answers, receive next question or completion

PUT

/api/forms/reset/{processId}

Reset a process — delete all data and restart

PUT

/api/forms/resume/{processId}

Resume a paused process — allow person modifications

GET

/api/forms/fields/{processDefinitionId}

Read-only: get all form field definitions without creating a session

GET

/api/forms/fields/membership-period/{id}

Read-only: get form fields for a membership period

GET

/api/forms/test

Start a single-field test session (dev/test only)

MembershipFormController

The membership controller handles multi-person membership enrolment with family membership logic.

Key Behaviours

  • Person loading — Loads the principal user and all managed (linked) persons as candidates

  • Family membership detectiononBeforeProcessing() checks person counts to determine if family membership options should be offered (requires 2+ adults AND 2+ children)

  • Membership criteria assignment — Assigns base membership criteria to each person before step iteration

  • Dynamic question enablement — Family-specific questions (MFM, MFA, MFC) are conditionally enabled based on family eligibility

  • Order creationonDone() creates membership records, assigns membership numbers, and generates orders

Registered FormField Types

Inherits all base types plus: MFM, MFA, MFC, MCS

EventFormController

The event controller handles event participant registration. It is simpler than the membership controller, focusing on event-specific data capture.

Key Behaviours

  • Event validation — Ensures the event is open for registration

  • Participant creation — Creates EventParticipant records on completion

  • Category and race type selection — Provides event-specific question types for selecting categories and race types

Registered FormField Types

Inherits all base types plus: ECA, ERT

TestFormController

The test controller enables front-end developers to test individual FormField types in isolation, without configuring a full ProcessDefinition.

Purpose

During development of new form questions (Epic #34), front-end developers need to integrate with specific FormField types. Creating a full ProcessDefinition for each field type under development creates a dependency bottleneck. The TestFormController eliminates this by allowing any FormField type to be tested via a single API call.

API Usage

# Start a test session for a text field
GET /api/forms/test?stepCode=TXT&required=true&valueMin=2&valueMax=50

# Start a test session for a radio button field with options
GET /api/forms/test?stepCode=RBO&required=true&options=["Option A","Option B","Option C"]

# Submit the answer (uses the standard next endpoint)
POST /api/forms/next
Body: { "processId": "<uuid>", "people": [...] }

Behaviour

  1. The GET endpoint creates a fresh ProcessDefinition (type "T") with a single ProcessStep configured from the request parameters

  2. A ProcessInstance is created and persisted

  3. The front-end receives a standard FormDTO with the question

  4. Submissions go through the existing POST /api/forms/next — the factory method detects type "T" and creates a TestFormController

  5. Since there is only one step, successful validation results in DONE status

Restrictions

  • Available in dev and test Spring profiles only — not available in production

  • Registers all FormField types (base + event + membership) to support testing any question

  • No domain-specific side effects (onDone performs cleanup, not order/membership creation)

Request Parameters

Parameter Required Description

stepCode

Yes

FormField type code (see step code reference table above)

required

No

Whether the field is required (default: true)

valueMin

No

Minimum value or length

valueMax

No

Maximum value or length

valuePattern

No

Regex validation pattern

options

No

JSON array of option values for choice-based fields

personDataKey

No

Person data key for initial value resolution