DEV Community

Velspark
Velspark

Posted on

How to Design a Config-Driven Frontend Architecture for Enterprise Applications

Enterprise applications often begin with a simple requirement.

Build a form.

Then another team needs a similar form with a few different fields. A second region requires different validations. One customer wants an additional approval step. Another business unit follows a slightly different workflow.

What started as one straightforward screen gradually becomes ten nearly identical implementations.

Each implementation has its own components, conditions, validations, permissions, and business rules. Developers begin copying existing screens and modifying them for every new requirement.

Initially, this feels faster.

Over time, it becomes one of the most expensive architectural decisions in the application.

A config-driven frontend architecture offers a different approach.

Instead of hardcoding every screen and workflow separately, the application defines reusable rendering and execution engines. Product-specific behaviour is described through configuration.

This allows the same frontend foundation to support different customers, regions, workflows, and business processes without rebuilding the application each time.

However, config-driven architecture is not simply about putting form fields inside a JSON file.

A successful implementation requires clear boundaries, strong schemas, predictable extension points, validation, versioning, observability, and careful control over complexity.

This article explains how to design such an architecture for large enterprise applications.

What Is a Config-Driven Frontend?

In a traditional frontend, behaviour is commonly defined directly inside components.

A developer may write:

<TextField
  label="Customer Name"
  required
  disabled={!canEdit}
/>
Enter fullscreen mode Exit fullscreen mode

Validation, visibility, permissions, and layout decisions may also be embedded in the same component.

In a config-driven frontend, the component becomes generic.

The behaviour is described separately:

{
"id": "customerName",
"type": "text",
"label": "Customer Name",
"required": true,
"permissions": {
"edit": ["admin", "sales_manager"]
}
}

The application reads this configuration and decides:

  • which component to render
  • what label to display
  • whether the field is required
  • who can edit it
  • where it appears
  • which validations apply
  • whether it is visible
  • how its value should be transformed

The same principle can be applied beyond forms.

Configuration can describe:

  • page layouts
  • tables
  • filters
  • dashboards
  • approval workflows
  • navigation
  • permissions
  • business rules
  • actions
  • reports
  • feature availability
  • regional variations

The frontend becomes an engine that interprets structured definitions.

Why Enterprise Applications Benefit from This Approach

Config-driven architecture is especially useful when an application contains repeated structures with controlled variations.

Consider a global enterprise platform used across several regions.

The core business process may remain the same, but each region may require:

  • different fields
  • different labels
  • different validation rules
  • different approval stages
  • different user roles
  • different reports
  • different regulatory requirements

Without a shared architecture, teams often create separate implementations.

EuropeCustomerForm.tsx
AsiaCustomerForm.tsx
NorthAmericaCustomerForm.tsx
EnterpriseCustomerForm.tsx
PartnerCustomerForm.tsx

These files may share 70 to 90 percent of their logic.

Yet every bug fix, accessibility improvement, design update, or performance optimisation must be applied repeatedly.

Configuration replaces duplication with variation.

CustomerFormEngine
├── Europe configuration
├── Asia configuration
├── North America configuration
├── Enterprise configuration
└── Partner configuration

The shared engine handles common behaviour. Configuration describes only what changes.

This can significantly improve:

  • delivery speed
  • consistency
  • maintainability
  • testing
  • scalability
  • onboarding
  • product customisation

But these benefits appear only when the architecture is designed carefully.

Start with the Right Problem

Not every frontend should be config-driven.

Configuration is valuable when variation is frequent, predictable, and structurally similar.

Good candidates include:

  • dynamic forms
  • multi-step workflows
  • configurable dashboards
  • permission-sensitive screens
  • repeated approval processes
  • customer-specific product variants
  • multi-region applications
  • internal enterprise platforms

Config-driven architecture is less useful when:

  • every screen is fundamentally unique
  • requirements change rarely
  • user experiences require highly custom interactions
  • the configuration becomes more complicated than normal code
  • only one implementation will ever exist

The goal is not to eliminate code.

The goal is to move repeatable variation out of application logic and into a controlled model.

A useful question is:

Are we repeatedly rebuilding the same structure with small business-specific differences?

If the answer is yes, configuration may be appropriate.

The Core Architectural Principle

A strong config-driven system separates three concerns.

  1. The Rendering Engine

The rendering engine knows how to display supported interface elements.

It understands:

  • text inputs
  • dropdowns
  • date pickers
  • tables
  • tabs
  • sections
  • modals
  • buttons
  • workflow steps

It should not know customer-specific business details.

  1. The Configuration

The configuration describes what should appear and how it should behave.

It may define:

  • field types
  • labels
  • layout
  • validation
  • visibility
  • permissions
  • actions
  • dependencies
  • workflow order
  1. The Business Services

Business services handle operations that should not live inside the configuration or UI engine.

These include:

  • API calls
  • calculations
  • authorisation checks
  • workflow transitions
  • domain validation
  • data transformation
  • audit logging

The architecture can be visualised as:

Configuration

Configuration Parser

Rendering and Workflow Engine

Business Services and APIs

The most important rule is this:

Configuration should describe behaviour, not become an uncontrolled programming language.

Once configuration starts containing arbitrary scripts, nested conditions, and complex domain logic, the system becomes difficult to understand and unsafe to maintain.

Define a Strong Configuration Schema

The schema is the contract between configuration authors and the frontend engine.

A weak schema creates ambiguity.

A strong schema creates predictability.

For example:

type FieldType =
  | "text"
  | "number"
  | "select"
  | "date"
  | "checkbox";
interface FieldConfig {
  id: string;
  type: FieldType;
  label: string;
  defaultValue?: unknown;
  required?: boolean;
  placeholder?: string;
  options?: SelectOption[];
  validation?: ValidationRule[];
  visibility?: VisibilityRule;
  permissions?: PermissionRule;
  layout?: LayoutConfig;
}
Enter fullscreen mode Exit fullscreen mode

A page configuration may look like:

interface PageConfig {
  id: string;
  version: number;
  title: string;
  sections: SectionConfig[];
  actions: ActionConfig[];
}
Enter fullscreen mode Exit fullscreen mode

Each level should have a clear responsibility.

Page
├── Sections
│ ├── Fields
│ └── Components
└── Actions

Avoid configurations where every object can contain every possible property.

For example, this is risky:

{
  "type": "anything",
  "settings": {
    "custom": {
      "data": {
        "value": "unknown"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Such structures appear flexible, but they sacrifice type safety, discoverability, and validation.

Prefer explicit definitions.

{
  "id": "country",
  "type": "select",
  "label": "Country",
  "optionsSource": {
    "type": "api",
    "endpoint": "/countries"
  }
}
Enter fullscreen mode Exit fullscreen mode

A good schema makes invalid states difficult to represent.

Use Discriminated Unions in TypeScript

Different component types require different properties.

A text field may have a maximum length. A select field needs options. A date field may require minimum and maximum dates.

TypeScript discriminated unions are ideal for this.

interface BaseFieldConfig {
  id: string;
  label: string;
  required?: boolean;
}
interface TextFieldConfig extends BaseFieldConfig {
  type: "text";
  minLength?: number;
  maxLength?: number;
}
interface SelectFieldConfig extends BaseFieldConfig {
  type: "select";
  options: Array<{
    label: string;
    value: string;
  }>;
}
interface DateFieldConfig extends BaseFieldConfig {
  type: "date";
  minDate?: string;
  maxDate?: string;
}
type FieldConfig =
  | TextFieldConfig
  | SelectFieldConfig
  | DateFieldConfig;
Enter fullscreen mode Exit fullscreen mode

Now the renderer can safely narrow the type.

function FieldRenderer({ config }: { config: FieldConfig }) {
  switch (config.type) {
    case "text":
      return <TextInput config={config} />;
    case "select":
      return <SelectInput config={config} />;
    case "date":
      return <DateInput config={config} />;
    default:
      return assertNever(config);
  }
}
Enter fullscreen mode Exit fullscreen mode

This provides compile-time protection when new field types are introduced.

Build a Component Registry

A component registry maps configuration types to implementations.

const fieldRegistry = {
  text: TextFieldRenderer,
  number: NumberFieldRenderer,
  select: SelectFieldRenderer,
  date: DateFieldRenderer,
  checkbox: CheckboxFieldRenderer
};
Enter fullscreen mode Exit fullscreen mode

The generic renderer then becomes simple.

function FieldRenderer({ config }: { config: FieldConfig }) {
  const Component = fieldRegistry[config.type];
  if (!Component) {
    return <UnsupportedField type={config.type} />;
  }
  return <Component config={config} />;
}
Enter fullscreen mode Exit fullscreen mode

This registry acts as a controlled extension point.

When a new field type is introduced:

  1. define its schema
  2. build its renderer
  3. register it
  4. add tests
  5. update documentation

This is far safer than allowing configuration to directly reference arbitrary component paths.

Avoid configurations such as:

{
  "component": "../../components/CustomInternalField"
}
Enter fullscreen mode Exit fullscreen mode

That tightly couples configuration to application internals and can create security, deployment, and maintainability problems.

Separate Layout from Field Definitions

A common mistake is mixing data definition and layout too tightly.

For example:

{
  "id": "email",
  "type": "text",
  "label": "Email",
  "marginLeft": 16,
  "width": "47%",
  "top": 132
}
Enter fullscreen mode Exit fullscreen mode

This makes the configuration fragile and difficult to adapt across devices.

Prefer semantic layout rules.

{
  "id": "email",
  "type": "text",
  "label": "Email",
  "layout": {
    "columnSpan": {
      "desktop": 6,
      "tablet": 12,
      "mobile": 12
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Or define layout at the section level.

{
  "id": "contactSection",
  "title": "Contact Information",
  "layout": {
    "type": "grid",
    "columns": 12,
    "gap": "medium"
  },
  "fields": [
    {
      "id": "email",
      "columnSpan": 6
    },
    {
      "id": "phone",
      "columnSpan": 6
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

The configuration should describe intent.

The design system should control visual implementation.

Design Validation as a First-Class Capability

Validation is one of the most important parts of a config-driven form system.

A field may require:

  • required validation
  • length validation
  • numeric ranges
  • pattern matching
  • cross-field validation
  • API-based validation
  • business-rule validation

Simple validation can be represented declaratively.

{
  "id": "employeeCount",
  "type": "number",
  "validation": [
    {
      "type": "required",
      "message": "Employee count is required."
    },
    {
      "type": "minimum",
      "value": 1,
      "message": "Employee count must be greater than zero."
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

The validation engine maps rule types to validators.

const validatorRegistry = {
  required: requiredValidator,
  minimum: minimumValidator,
  maximum: maximumValidator,
  pattern: patternValidator
};
Enter fullscreen mode Exit fullscreen mode

However, not every business rule belongs in configuration.

Suppose approval eligibility depends on contract type, account status, region, historical transactions, and data from three backend systems.

Encoding this entire rule inside frontend configuration would be a mistake.

The frontend may trigger the validation, but the authoritative rule should remain in a domain service or backend API.

A useful separation is:

Simple UI validation → configuration
Cross-field UI validation → controlled rule engine
Critical business validation → backend or domain service

Handle Conditional Visibility Carefully

Conditional visibility is useful but can quickly become complex.

A simple rule might be:

{
  "id": "companyName",
  "type": "text",
  "visibility": {
    "field": "customerType",
    "operator": "equals",
    "value": "business"
  }
}
Enter fullscreen mode Exit fullscreen mode

A more flexible rule structure may support groups.

{
  "visibility": {
    "all": [
      {
        "field": "customerType",
        "operator": "equals",
        "value": "business"
      },
      {
        "field": "country",
        "operator": "in",
        "value": ["US", "CA"]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

The evaluation engine should support a limited and documented set of operators.

For example:

type Operator =
  | "equals"
  | "notEquals"
  | "in"
  | "notIn"
  | "greaterThan"
  | "lessThan"
  | "isEmpty"
  | "isNotEmpty";
Enter fullscreen mode Exit fullscreen mode

Avoid storing JavaScript expressions inside configuration.

{
  "visibleWhen": "values.customerType === 'business'"
}
Enter fullscreen mode Exit fullscreen mode

This may seem convenient, but it introduces several problems:

  • security risks
  • difficult debugging
  • weak type safety
  • inconsistent evaluation
  • poor validation
  • limited tooling support

Use structured conditions instead of executable strings.

Treat Permissions as More Than Visibility

Hiding a button is not security.

A configuration may describe interface-level permissions.

{
  "id": "deleteCustomer",
  "type": "action",
  "permissions": {
    "visibleFor": ["admin"],
    "enabledFor": ["admin"]
  }
}
Enter fullscreen mode Exit fullscreen mode

This improves the user experience, but the backend must still enforce authorisation.

A robust system uses multiple layers:

Configuration

Frontend permission evaluation

Backend authorisation

Audit logging

Frontend permissions answer:

  • should the user see this element?
  • should the element be editable?
  • should an action appear disabled?

Backend permissions answer:

  • is the user legally and operationally allowed to perform the action?

Never rely on configuration as the final security boundary.

Separate Actions from Components

Buttons and actions should be described independently from their implementation.

{
  "id": "submitApplication",
  "type": "submit",
  "label": "Submit",
  "confirmation": {
    "enabled": true,
    "message": "Are you sure you want to submit this application?"
  },
  "successBehaviour": {
    "type": "navigate",
    "target": "/applications"
  }
}
Enter fullscreen mode Exit fullscreen mode

An action handler registry can map supported action types.

const actionRegistry = {
  submit: handleSubmit,
  saveDraft: handleSaveDraft,
  navigate: handleNavigate,
  openModal: handleOpenModal
};
Enter fullscreen mode Exit fullscreen mode

This creates a controlled interaction model.

The configuration chooses from supported behaviours. It does not implement the behaviour itself.

Config-Driven Workflows

The same architecture can support multi-step business workflows.

{
  "workflowId": "vendorOnboarding",
  "version": 3,
  "steps": [
    {
      "id": "companyDetails",
      "title": "Company Details",
      "pageConfigId": "vendor-company-details"
    },
    {
      "id": "compliance",
      "title": "Compliance",
      "pageConfigId": "vendor-compliance",
      "visibleWhen": {
        "field": "requiresCompliance",
        "operator": "equals",
        "value": true
      }
    },
    {
      "id": "review",
      "title": "Review and Submit",
      "pageConfigId": "vendor-review"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

The workflow engine manages:

  • current step
  • completed steps
  • navigation rules
  • validation before progression
  • skipped steps
  • saved state
  • workflow status
  • resume behaviour

It is important to distinguish frontend workflow navigation from business workflow state.

The frontend may control which screen appears next.

The backend should usually own critical state transitions such as:

Draft → Submitted → Under Review → Approved

This prevents users from manipulating business state through frontend configuration.

Decide Where Configuration Lives

Configuration can come from several sources.

Bundled with the Frontend

Configuration is stored inside the codebase.

src/config/customers/europe.ts
src/config/customers/asia.ts

Advantages:

  • type safety
  • version control
  • code review
  • easy testing
  • predictable deployments

Disadvantages:

  • configuration changes require deployment
  • non-developers cannot easily modify it

Served by a Backend

The frontend retrieves configuration through an API.

GET /api/ui-config/customer-onboarding?region=eu

Advantages:

  • configuration can change independently
  • customer-specific variants are easier
  • centralised governance
  • feature rollout becomes more flexible

Disadvantages:

  • runtime failures are possible
  • schema compatibility becomes critical
  • caching and availability must be handled
  • debugging becomes more complex

Hybrid Model

A hybrid approach often works best.

The application bundles a safe default configuration while allowing controlled overrides from the backend.

Base configuration
+
Region override
+
Customer override
+
Feature flags
=
Resolved configuration

However, configuration merging must be explicit.

Uncontrolled deep-merging can produce surprising results.

Prefer deterministic strategies such as:

  • replace section
  • append field
  • remove field by ID
  • override specific property
  • extend validation rules

Build a Configuration Resolution Layer

Large enterprise systems rarely use one configuration file directly.

The final configuration may depend on:

  • application version
  • customer
  • region
  • tenant
  • role
  • feature flag
  • product edition
  • environment

Do not spread this resolution logic throughout React components.

Create a dedicated resolver.

interface ConfigurationContext {
  tenantId: string;
  region: string;
  roles: string[];
  features: Record<string, boolean>;
}
function resolvePageConfig(
  baseConfig: PageConfig,
  overrides: PageOverride[],
  context: ConfigurationContext
): PageConfig {
  // Resolve applicable configuration deterministically.
}
Enter fullscreen mode Exit fullscreen mode

The component should receive an already resolved configuration.

<PageRenderer config={resolvedConfig} />
Enter fullscreen mode Exit fullscreen mode

It should not decide which regional override or customer variation to apply.

This keeps rendering predictable and testable.

Validate Configuration Before Rendering

Configuration is input.

Like any input, it can be invalid.

A malformed configuration may contain:

  • duplicate IDs
  • unknown component types
  • invalid validation rules
  • broken field references
  • unsupported operators
  • missing required properties
  • circular dependencies
  • invalid workflow transitions

Runtime schema validation should happen before rendering.

Libraries such as Zod, JSON Schema, or similar validation tools can help.

const pageConfigSchema = z.object({
  id: z.string(),
  version: z.number(),
  title: z.string(),
  sections: z.array(sectionConfigSchema)
});
Enter fullscreen mode Exit fullscreen mode

Then:

const result = pageConfigSchema.safeParse(rawConfig);
if (!result.success) {
  reportConfigurationError(result.error);
  return fallbackConfig;
}
Enter fullscreen mode Exit fullscreen mode

Do not allow invalid configuration to fail silently.

In enterprise systems, a safe fallback is often better than a blank screen.

Introduce Configuration Versioning Early

Configuration evolves alongside the application.

Version 1 may support basic fields.

Version 2 may introduce conditional visibility.

Version 3 may change the validation model.

Without versioning, older configurations can break when the frontend engine changes.

Include a schema version.

{
  "schemaVersion": 3,
  "id": "customer-onboarding",
  "version": 12
}
Enter fullscreen mode Exit fullscreen mode

These two versions can represent different concepts:

  • schemaVersion: the structure understood by the frontend engine
  • version: the business configuration revision

The application can then migrate older schemas.

function migrateConfig(rawConfig: UnknownConfig): CurrentPageConfig {
if (rawConfig.schemaVersion === 1) {
return migrateV1ToV2(rawConfig);
}
if (rawConfig.schemaVersion === 2) {
return migrateV2ToV3(rawConfig);
}
return rawConfig;
}

Backward compatibility should be treated as a product capability, not an afterthought.

Manage State by Responsibility

A config-driven application may involve several forms of state.

They should not all be placed in one global store.

Form State

Contains field values, errors, touched state, and dirty state.

Workflow State

Contains the current step, completed steps, and navigation status.

Server State

Contains data fetched from APIs.

UI State

Contains modal visibility, active tabs, expanded sections, and notifications.

Configuration State

Contains the active configuration, resolved overrides, and version metadata.

Separating these responsibilities prevents the system from becoming tightly coupled.

A possible structure is:

React Hook Form or form engine → field state
React Query or equivalent → server state
Workflow context → workflow navigation
Local component state → temporary UI state
Config provider → resolved configuration

The specific libraries are less important than the separation of concerns.

Performance Considerations

Config-driven rendering introduces additional work.

The application must:

  • parse configuration
  • resolve overrides
  • evaluate conditions
  • map types to components
  • calculate dependencies
  • generate layouts
  • validate values

For small forms, this is rarely an issue.

For large enterprise screens with hundreds of fields, performance must be designed deliberately.

Useful techniques include:

Memoise Resolved Configuration

Do not resolve the same configuration on every render.

const resolvedConfig = useMemo(
() => resolvePageConfig(baseConfig, overrides, context),
[baseConfig, overrides, context]
);

Track Field Dependencies

If one field depends on another, subscribe only to the required value.

Do not re-evaluate every rule whenever any field changes.

customerType → companyName visibility
country → taxIdentifier validation
contractType → approval section visibility

A dependency graph can identify which rules need re-evaluation.

Virtualise Large Collections

Large configurable tables and long forms may benefit from virtualisation.

Lazy-Load Heavy Components

Rich-text editors, chart libraries, document viewers, and advanced selectors should be loaded only when needed.

Cache Remote Configuration

Cache configuration using a version or hash while preserving a safe invalidation strategy.

Avoid Excessive Dynamic Abstraction

Every layer of indirection adds runtime and cognitive cost.

Keep the rendering pipeline understandable.

Build for Observability

When a traditional hardcoded screen fails, a developer can inspect the component.

When a config-driven screen fails, the issue may come from:

  • configuration
  • schema mismatch
  • an override
  • the resolver
  • a renderer
  • user context
  • a feature flag
  • remote data
  • an unsupported rule

Observability is therefore essential.

Log useful metadata such as:

pageConfigId
configurationVersion
schemaVersion
tenantId
region
componentType
fieldId
ruleId
featureFlags

For example:

reportConfigError({
pageConfigId: config.id,
version: config.version,
fieldId: field.id,
error: "Unsupported field type",
type: field.type
});

A developer mode can also show:

  • resolved configuration
  • active overrides
  • evaluated visibility rules
  • field dependencies
  • configuration source
  • current version

This can reduce debugging time significantly.

Testing Strategy

A config-driven system requires testing at several levels.

Schema Tests

Verify that valid configurations are accepted and invalid configurations are rejected.

Renderer Tests

Test every registered field, section, layout, and action type independently.

Rule Engine Tests

Test visibility, validation, permissions, and condition operators.

Resolution Tests

Test regional, customer, tenant, and feature-based overrides.

Contract Tests

Verify that backend configuration remains compatible with the frontend schema.

Snapshot Tests

Snapshots can be useful for resolved configuration structures, but should not replace behavioural tests.

End-to-End Tests

Test critical workflows from configuration loading to submission.

A practical test matrix may look like:

Configuration schema
Component registry
Validation rules
Visibility rules
Permission rules
Configuration merging
Workflow navigation
Submission behaviour
Backward compatibility
Fallback behaviour

The key advantage is that the same rendering engine can be tested once and reused across many product variations.

Governance and Ownership

As config-driven systems grow, configuration becomes part of the product.

It requires governance.

Teams should define:

  • who can create configuration
  • who reviews it
  • how it is validated
  • how changes are deployed
  • how rollbacks work
  • how versions are tracked
  • how deprecated properties are removed
  • how configuration is documented

Without governance, configuration files can become a second uncontrolled codebase.

A mature workflow may include:

Author configuration

Validate schema

Run automated tests

Preview in sandbox

Review and approve

Publish version

Monitor

Configuration changes should be traceable.

For critical enterprise workflows, audit metadata may include:

  • author
  • reviewer
  • publication time
  • previous version
  • change summary
  • affected tenants
  • rollback version

Build an Internal Configuration Studio Carefully

Once configuration becomes valuable, organisations often want a visual editor.

A configuration studio can allow product teams or administrators to:

  • add fields
  • rearrange sections
  • define validations
  • configure permissions
  • preview workflows
  • publish changes

This can be powerful, but it should come after the schema and engine are stable.

Building the visual editor too early often causes the architecture to optimise around incomplete assumptions.

A better progression is:

Typed code configuration

Validated JSON configuration

Remote configuration service

Visual configuration studio

The visual editor should generate the same validated schema used by developers.

It should not create a separate configuration format.

Avoid the Most Common Failure Modes

Failure 1: Turning Configuration into Code

When configuration contains scripts, expressions, callbacks, and arbitrary logic, it becomes harder to test and secure than normal code.

Keep the supported language small and structured.

Failure 2: Making Everything Configurable

Not every margin, colour, animation, and internal behaviour needs to be configurable.

Expose only meaningful product variation.

Failure 3: Building One Giant Renderer

A single component containing hundreds of conditions becomes impossible to maintain.

Use registries and specialised renderers.

Failure 4: Ignoring Versioning

Remote configurations and frontend releases will eventually move at different speeds.

Version the schema from the beginning.

Failure 5: Mixing Business Logic with UI Configuration

Critical decisions should remain in domain services or backend systems.

Configuration should orchestrate supported behaviour, not own the entire business domain.

Failure 6: Weak Error Handling

An invalid configuration should not produce an unexplained blank page.

Validate, log, report, and provide safe fallbacks.

Failure 7: Uncontrolled Overrides

Layering tenant, region, role, and feature overrides without deterministic rules creates unpredictable behaviour.

Define an explicit resolution order.

Failure 8: No Developer Tooling

Without previews, schema documentation, diagnostics, and configuration inspection, teams will struggle to operate the system.

A Practical Folder Structure

A scalable React and TypeScript implementation may look like this:

src/
├── config-engine/
│ ├── schemas/
│ │ ├── page.schema.ts
│ │ ├── field.schema.ts
│ │ ├── workflow.schema.ts
│ │ └── validation.schema.ts
│ │
│ ├── registry/
│ │ ├── field-registry.ts
│ │ ├── action-registry.ts
│ │ ├── validator-registry.ts
│ │ └── layout-registry.ts
│ │
│ ├── renderers/
│ │ ├── PageRenderer.tsx
│ │ ├── SectionRenderer.tsx
│ │ ├── FieldRenderer.tsx
│ │ └── ActionRenderer.tsx
│ │
│ ├── rules/
│ │ ├── evaluate-condition.ts
│ │ ├── evaluate-permission.ts
│ │ └── resolve-dependencies.ts
│ │
│ ├── resolution/
│ │ ├── resolve-config.ts
│ │ ├── apply-overrides.ts
│ │ └── migrate-config.ts
│ │
│ ├── validation/
│ │ ├── validate-config.ts
│ │ └── validate-field-value.ts
│ │
│ └── observability/
│ ├── config-logger.ts
│ └── diagnostics.ts

├── components/
│ ├── fields/
│ ├── layouts/
│ └── actions/

├── business-services/
│ ├── customer.service.ts
│ ├── workflow.service.ts
│ └── permissions.service.ts

└── configurations/
├── base/
├── regions/
└── tenants/

This structure keeps the engine, business logic, reusable components, and business configurations separate.

A Step-by-Step Adoption Strategy

Config-driven architecture does not need to be introduced across the entire application immediately.

A gradual approach is safer.

Step 1: Identify Repetition

Find two or three screens that share most of their structure.

Step 2: Extract a Shared Renderer

Move repeated rendering logic into reusable components.

Step 3: Define a Small Typed Schema

Support only the capabilities currently required.

Step 4: Introduce a Registry

Map known configuration types to controlled implementations.

Step 5: Add Validation

Validate all configuration before rendering.

Step 6: Add Conditions and Permissions

Introduce them only when real use cases appear.

Step 7: Add Versioning

Do this before configuration is stored remotely.

Step 8: Add Remote Delivery

Move configuration outside the frontend deployment only when operational flexibility is needed.

Step 9: Add Tooling

Build previews, diagnostics, documentation, and eventually visual authoring tools.

The architecture should grow from concrete requirements rather than imagined flexibility.

The Most Important Design Boundary

The success of a config-driven frontend depends on one architectural boundary:

The engine owns reusable capabilities. Configuration selects and combines those capabilities. Business services own domain truth.

The engine may know how to render a date picker.

Configuration may decide that the date picker represents a contract start date.

A business service should decide whether that contract date is legally valid.

The engine may know how to display an approval step.

Configuration may decide when the approval step appears.

The backend should decide whether the approval is authorised and record the transition.

Keeping these responsibilities separate allows the system to remain flexible without becoming unpredictable.

Final Thoughts

A config-driven frontend can transform how enterprise applications are built.

Instead of creating a new implementation for every customer, region, workflow, or product variation, teams can invest in a reusable platform.

That platform can deliver:

  • consistent user experiences
  • faster implementation
  • safer changes
  • reusable business capabilities
  • better scalability
  • reduced duplication

But flexibility has a cost.

Every configuration option expands the system’s language. Every rule adds complexity. Every override creates another possible execution path.

The objective should therefore not be maximum configurability.

The objective should be controlled configurability.

A good config-driven architecture does not allow configuration to do everything.

It provides a clear, typed, validated, observable, and versioned set of capabilities that can be safely combined to solve recurring business problems.

When designed this way, configuration is no longer just a collection of JSON files.

It becomes a scalable product platform for building enterprise experiences.

Top comments (0)