The core of the application is a design wizard — a multi-page form that walks environmental professionals through configuring a septic system. The wizard has roughly 30 possible pages. Not all of them appear in every session. The sequence depends on what the user selects.

Choose an aerobic primary treatment? You’ll see pages for the aerobic unit configuration. Choose a septic tank? Different pages. Select UV disinfection? A UV configuration page appears. Choose no disinfection? That page is skipped entirely.

The page flow isn’t a fixed sequence. It’s a state machine.

The architecture

Three pieces make it work:

The base classTWizPage. Every wizard page inherits from this and implements four methods: Init (set up controls), SetData (populate from the domain model), TestData (validate input), and StoreData (write back to the domain model). There’s also OptionsChanged for reacting to upstream changes.

The base class has class variables — shared across all page instances — for the data model, resource dictionaries, configuration options, and a readiness flag. This means any page can read any other page’s stored data. It’s global state, deliberately.

The state machineSewerWizardLogic.pas. A function that takes the current page name and the direction (forward or back) and returns the next page name. It examines the current system configuration to decide: “Given what the user has selected so far, what page comes next?”

The logic is a cascade of conditionals. If the system type includes secondary treatment, show the secondary treatment pages. If it includes disinfection, show those. If the discharge type is a bed, show bed configuration; if it’s a stream, show stream configuration. Each decision point reads from the shared data model.

The containeruDesignWizard.pas. Manages the page stack, creates form instances, handles forward/back navigation. When the user clicks “Next,” it calls TestData on the current page, and if validation passes, calls StoreData, then asks the state machine for the next page.

The preservation problem

Here’s the subtle part. When a user goes back and changes an early selection — say, switching from UV disinfection to chlorine — downstream pages need to know. But the user might have already filled in pages that are still valid (like the discharge configuration, which doesn’t depend on disinfection type).

Each page implements a hash-based change detection: CanPreservePreviousSettings(hashName, hashValue). When entering a page, it computes a hash of the upstream configuration that affects it. If the hash matches what it was last time, the page keeps its previous values. If it doesn’t match, the page resets.

This means you can go back, change the primary treatment type, come forward again, and pages that don’t depend on primary treatment will still have your previous entries. Pages that do depend on it will reset and ask you to reconfigure.

The component hierarchy

The domain model behind the wizard is a polymorphic component tree. A septic system composes:

  • A primary treatment (7 possible types)
  • A secondary treatment (9 possible types)
  • A disinfection method (3 possible types)
  • A discharge method (12+ possible types)
  • Tanks, pumps, dosing systems, filters

Each is a strategy object with its own calculations, validations, and HTML generation. The wizard pages configure these objects. The state machine determines which pages appear based on which strategy objects are active.

It’s essentially a product configurator for underground wastewater treatment systems. The domain is niche. The architecture is surprisingly general.

What makes it hard

The combinatorial explosion. Seven primary types times nine secondary types times three disinfection types times twelve discharge types is over 2,000 possible configurations. Not all are valid — a rules engine (SystemMatrix.pas / SystemRule.pas) validates component compatibility. But the wizard needs to present a coherent path through any valid combination.

Testing is manual. There are no automated tests. The developer runs through configurations by hand, checking that pages appear in the right order, validations fire correctly, and the final output is accurate. It’s the kind of system where automated testing would pay for itself immediately — but the framework doesn’t lend itself to headless testing, and the domain knowledge required to write meaningful test cases is itself specialized.

So we rely on the state machine being correct, the hash-based preservation being reliable, and the developer knowing the domain well enough to spot when something’s wrong. It works. It’s fragile in a way that’s hard to quantify.