Killing Shotgun Surgery in High‑Compliance React Registration Forms: A Real-World Case Study
(Layering policy, presentation & integration to localize change)
Problem in one line: A single password / KYC / geo rule tweak forces many tiny edits across components, store logic, util files, and API routes — the Shotgun Surgery code smell.
Real-World Context: High-Compliance Registration Form
This case study examines the refactoring of a high-compliance Australian P2P online exchange platform’s registration form, requiring strict KYC (Know Your Customer) validation. The form handles:
- Australian/New Zealand compliance with different phone formats (+61 vs +64)
- Age verification (18+ requirement with document backup)
- Password security with breach database checks
- Address validation for identity verification
- Transaction limit controls for responsible trading
The original implementation suffered from scattered validation logic across React components, Zustand stores, and API routes — a perfect example of the Shotgun Surgery anti-pattern. Note on validation libraries: We deliberately show a manual “policy module” approach, but libraries like Zod, Yup, Joi, AJV (or io‑ts) can further reduce Shotgun Surgery by unifying runtime validation and (for Zod / io‑ts) TypeScript inference, while Joi/Yup offer rich, declarative schemas—trade‑offs revolve around bundle size, TS inference fidelity, and built‑in validators vs. bespoke domain predicates you still must write (e.g., personal‑info exclusion).
Why this matters here: Online P2P exchanges in Australia now require pre‑verification (identity confirmed before account use), shrinking the time you have to ship policy changes. The registration form must handle rapid regulatory updates while maintaining user experience.
Password guidance is shifting toward length, compromised credential screening, and avoiding frequent forced resets, de‑emphasizing arbitrary symbol mixtures.
Attackers exploit personal “cribs” (names, birthdays, phone digits), so excluding personal information strengthens defenses while aligning with modern usability‑centric recommendations.
Country‑specific phone/address formats (e.g., AU vs NZ) introduce branching that, if duplicated, multiplies the blast radius of later regulatory or market expansion changes.
1. Layered Architecture Framing (Mirroring Fowler’s Modularization Journey)
The Fowler‑site modularization approach advocates extracting domain & data concerns from React components so each change touches fewer modules.
We map to explicit layers to localize volatility (password constants, identity thresholds, geo regex) into a Domain Policy seam.
Layer | Responsibility | Example Artifacts |
---|---|---|
Presentation | Render & minimal UI state | <PasswordInput/> , <PasswordChecklist/> |
Domain Policy | Business rules & predicates | passwordPolicy.ts , geo/phone.ts |
Integration / Adapters | External calls / side effects | ratePassword() adapter, api/register |
State Orchestration | Aggregate rule outcomes | registrationSlice |
Localizing volatility embodies modularization principles: developers work only in the policy file when rules shift.
Conceptual Layering Diagram
┌─────────────────┐ ┌─────────────────┐
│ Presentation │◄──►│ State Store │
│ (thin components)│ │ (Zustand) │
└─────────────────┘ └─────────────────┘
│ │
└───────┬───────────┘
│
┌─────────────────────────────────┐
│ Domain Policy / Rules │
│ passwordPolicy│kycPolicy│geo/phone│
└─────────────────────────────────┘
│
┌─────────────────────────┐
│ Integration Adapters │
│ ratePassword API call │
│ external KYC services │
└─────────────────────────┘
Sequence Diagram: Password Validation Flow
Presentation Layer State Store Domain Policy Integration
│ │ │ │
│──validate()───────►│ │ │
│ │──evaluate()─────►│ │
│ │ │──check()─────►│
│ │ │◄──result──────│
│ │◄──validated─────│ │
│◄──feedback─────────│ │ │
TypeScript Implementation Snippets
Domain Policy Layer:
// passwordPolicy.ts
export interface PasswordRule {
id: string;
description: string;
validate: (password: string, userInfo: UserInfo) => boolean;
}
export const passwordRules: PasswordRule[] = [
{
id: 'length',
description: 'At least 12 characters',
validate: (pwd) => pwd.length >= 12
},
{
id: 'personal-info',
description: 'No personal information',
validate: (pwd, userInfo) => !containsPersonalInfo(pwd, userInfo)
},
{
id: 'breach-check',
description: 'Not in breach database',
validate: async (pwd) => !(await checkBreachDatabase(pwd))
}
];
export const evaluatePassword = async (
password: string,
userInfo: UserInfo
): Promise<ValidationResult> => {
const results = await Promise.all(
passwordRules.map(rule => rule.validate(password, userInfo))
);
return {
isValid: results.every(Boolean),
failedRules: passwordRules.filter((_, i) => !results[i])
};
};
Presentation Layer:
// PasswordInput.tsx
export const PasswordInput: React.FC = () => {
const [password, setPassword] = useState('');
const { validatePassword, validationState } = usePasswordValidation();
const handleChange = (value: string) => {
setPassword(value);
validatePassword(value);
};
return (
<div>
<input
type="password"
value={password}
onChange={(e) => handleChange(e.target.value)}
/>
<PasswordChecklist rules={validationState.failedRules} />
</div>
);
};
State Store Layer:
// registrationStore.ts
interface RegistrationState {
password: string;
validationState: ValidationResult;
isValid: boolean;
}
export const useRegistrationStore = create<RegistrationState>((set, get) => ({
password: '',
validationState: { isValid: false, failedRules: [] },
isValid: false,
validatePassword: async (password: string) => {
const userInfo = get().userInfo;
const result = await evaluatePassword(password, userInfo);
set({
password,
validationState: result,
isValid: result.isValid
});
}
}));
Integration Adapter Layer:
// passwordRatingAdapter.ts
export const ratePassword = async (password: string): Promise<number> => {
// Short-circuit if local validation fails
const localResult = await evaluateLocalRules(password);
if (!localResult.isValid) {
return 0; // Don't waste API calls on obviously weak passwords
}
try {
const response = await fetch('/api/rate-password', {
method: 'POST',
body: JSON.stringify({ password })
});
return await response.json();
} catch (error) {
console.error('Password rating service unavailable');
return localResult.score; // Fallback to local scoring
}
};
Platform-Specific Implementation Examples
Phone Number Validation Strategy
// geo/phone.ts
export const phoneValidators = {
'AU': {
pattern: /^04\d{8}$/,
format: '+61 4 XXXX XXXX',
example: '04 1234 5678'
},
'NZ': {
pattern: /^02\d{7}$/,
format: '+64 2 XXX XXXX',
example: '02 1234 567'
}
};
export const validatePhoneNumber = (phone: string, country: 'AU' | 'NZ') => {
const validator = phoneValidators[country];
return validator.pattern.test(phone.replace(/\s/g, ''));
};
Age Verification Policy
// kycPolicy.ts
export const ageVerificationPolicy = {
minimumAge: 18,
requireDocuments: (dob: Date) => {
const age = calculateAge(dob);
return age < 18 || !canVerifyElectronically(dob);
},
documentTypes: ['passport', 'drivers-license', 'medicare-card']
};
Deposit Limit Controls
// responsibleTrading.ts
export const transactionLimitPolicy = {
maxMonthlyLimit: 10000, // AUD
defaultPeriod: 'month',
periods: ['day', 'week', 'month'],
resetSchedule: 'end-of-period'
};
2. “Before” – The Shotgun Smell (Expanded Snapshot)
Deadline pressure led to intermixed validation, UI, and side effects; a single rule change (length 10→12, new personal substring window) required edits in each divergent locus.
2.1 Coupled Component (UI + Rules + Debounce + Personal Info)
Inline regex and personal‑info checks cohabit the component; changing any rule means editing JSX + logic entangled with render cycle.
2.2 Divergent “validators” Util (Conflicting Constants)
A separate util defines a different minimum length and symbol class—duplication invites inconsistency and missed updates.
2.3 Zustand Slice (Predicate Recomputed Broadly)
Global predicate duplication triggers unnecessary re‑renders and widens change surface.
2.4 API Route Re‑implements Partial Checks
Server logic repeats yet another variant (different min length), risking drift between client feedback and enforcement.
Blast Radius Example: Adjusting min length or personal substring window touches component, util, store, API, copy, and possibly tests (≥5 files).
3. “After” – Central Policy + Thin Presentation + Adapter
We extract rules into a single passwordPolicy module and isolate geo phone logic; components consume metadata; store & API call the same predicates; external strength API is wrapped with a short‑circuit for local failures.
3.1 Domain Policy (Single Source of Truth)
Central policy encapsulates length, required classes, personal‑info exclusion, and external scoring threshold in one change locus.
3.2 Geo Strategy (AU vs NZ Formatting Localized)
A strategy map replaces scattered if(country)
regex blocks—adding markets becomes additive, not invasive.
3.3 Thin Presentation Components
Components now render from rule metadata only; removing a rule means deleting one object in the policy array (automatic UI update).
3.4 Store & API Reuse Predicates (No Duplication)
Shared predicates guarantee uniform validation semantics and consistent user feedback vs. enforcement.
4. Benefits (Mapped to Modularization Themes)
Benefit | Mechanism | Source Alignment |
---|---|---|
Localized Change | Single policy module centralizes volatility | |
Cohesion & DRY | One definition drives UI + store + API | |
Lower Cognitive Load | Dev edits one file per rule tweak | |
Performance | Slim components reduce render churn | |
Security / Password Modernization | Length + breach/personal exclusion prioritized | |
Regulatory Agility | Pre‑verification thresholds become constant edits | |
Market Expansion | Strategy map for geo validators |
5. Incremental Refactor Journey
An evolutionary path (mirroring modularization guidance) avoids big‑bang rewrites while shrinking blast radius stepwise.
- Extract Constants: Lift
MIN_LENGTH
, geo regex to policy / geo modules (no behavior change yet). - Introduce Metadata: Replace inline password bullet JSX with
passwordRules
array. - Move Personal‑Info Predicate: Centralize substring logic; components call
evaluateLocal
. - Add External Adapter: Short‑circuit
ratePassword
if local rules fail (latency & cost win). - Thin the Store: Replace inline predicate with calls to shared modules.
- Delete Redundancy: Remove old util & coupled component logic after parity tests.
6. Performance & Re‑Render Reduction
Fat components recomputing large predicates per keystroke impede scalability; extracting pure, referentially transparent functions simplifies memoization and narrows subscriptions.
Short‑circuiting external password rating avoids unnecessary network latency and reduces server/API cost surface.
7. Security & Compliance Alignment
Central policy reflects modern recommendations: emphasize length/usability, perform compromised credential screening, exclude personal info, and avoid arbitrary frequent resets—while accommodating rapid AML/CTF pre‑verification rule shifts.
8. Micro‑Heuristics to Prevent Regression
- Grep Test: If
MIN_LENGTH
or a country regex appears in >2 places, refactor immediately. - Files‑Per‑Change Metric: Track & target ≤2 files touched for a policy tweak.
- Metadata‑Driven UI: Checklist bullets must come from
passwordRules
, never hard‑coded. - Short‑Circuit External Calls: Skip rating unless local rules all pass.
- Version Tag: Emit a
PASSWORD_POLICY_VERSION
in logs for audit under AML/CTF scrutiny.
9. Takeaway
By extracting domain policy modules, slimming components, and standardizing integration adapters, we apply modularization principles to neutralize Shotgun Surgery: a future shift (length 12→14, new personal substring window, additional market) becomes a single, low‑risk diff instead of a scattershot PR.
10. See It In Action
Experience the improved user experience firsthand by visiting the platform’s registration form. Notice how the form seamlessly handles:
- Real-time password validation with visual feedback
- Country-specific phone formatting (AU vs NZ)
- Age verification with clear document requirements
- Responsible trading controls with transaction limits
- Smooth error handling and user guidance
The refactored architecture enables rapid regulatory compliance updates while maintaining excellent user experience — exactly what modern, high-compliance applications need.