Configuration is Not Activation: A Pattern for Flexible Feature Management

On separating two concerns that are easy to conflate — and why the distinction pays off.


The Conflation

When two concerns appear related, the natural pull is to merge them. Configuration and activation feel related — after all, you usually configure something around the same time you want it running. So they end up in the same object, the same constructor, the same call.

But related is not the same as identical. And merging them quietly removes a capability you’ll want later.


The Core Distinction

Configuration answers: how should this feature behave when it’s running?

Activation answers: should this feature be running right now?

These are genuinely different questions with different lifecycles. Configuration tends to be stable — it reflects intent, business rules, integration contracts. Activation is contextual and operational — it responds to where something is rendered, what stage a rollout is in, what a user is allowed to do, or whether a service is ready.

Think of it like a router. It holds its settings — firewall rules, port mappings, retry policies — regardless of whether it’s powered on. Turning it off doesn’t erase the configuration. Turning it back on doesn’t require reconfiguring it. The two operations are orthogonal by design.

Software features should work the same way.


A Frontend Example: The File Component

Consider a file display component. It receives a document object — including a downloadUrl. The naive design drives the download button’s visibility from the data itself: if downloadUrl is present, show the button.

<!--Data presence drives feature visibility -->
@if (downloadSrc()) {
  <app-download-button
    [src]="downloadSrc()!"
    [filename]="filename()" />
}

This seems economical. But it couples two independent decisions: does this document have a download URL (data) and should a download button be shown here (context).

Now imagine two different screens that both use this component. On one, the full document toolbar makes sense — download, copy link, share. On another, it’s a compact read-only summary panel where a download button would be out of place — but copying the URL for internal use is still valid. The document is the same. The context is not.

With the data-driven approach you’re stuck: you either conditionally hide the button in the parent (leaking rendering logic upward), null out the downloadUrl before passing it in (lying about the data), or add a hideDownload boolean (an escape hatch that signals the model is already wrong).

The cleaner design separates configuration from activation explicitly:

export type FileCardFeature = 'download' | 'copyLink' | 'delete';

@Component({ selector: 'app-uploaded-file-card', ... })
export class UploadedFileCardComponent {
  // Configuration — describes the data
  filename = input.required<string>();
  document = input<Document | null>(null);

  // Activation — describes what this instance should do
  features = input<FileCardFeature[]>([]);

  protected hasFeature(feature: FileCardFeature) {
    return this.features().includes(feature);
  }
}
<!--Feature flags drive visibility, data drives behavior -->
@if (hasFeature('download')) {
  <app-download-button
    [src]="document()!.downloadUrl"
    [filename]="filename()" />
}
@if (hasFeature('copyLink')) {
  <app-copy-link-button [url]="document()!.downloadUrl" />
}

And at the call-site, activation is an explicit, readable decision:

<!-- Full contextall features active -->
<app-uploaded-file-card
  [filename]="doc.name"
  [document]="doc"
  [features]="['download', 'copyLink', 'delete']" />

<!-- Read-only summary panelno actions -->
<app-uploaded-file-card
  [filename]="doc.name"
  [document]="doc"
  [features]="[]" />

The document hasn’t changed. The downloadUrl is still there. The context decides what’s exposed — and that decision lives where it belongs: at the call-site, not inside the component, and not derived from data.


The Same Pattern on the Backend

The same logic applies to services. A service can be injected, configured, and present in the container — without being operationally active. Configuration reflects intent and contract. Activation reflects readiness and context.

// Full separation — maximum clarity
const service = new PaymentService();

service.configure({
  provider: 'stripe',
  webhookUrl: '...',
  retryPolicy: { maxRetries: 3 },
});

service.activatePayments();

// Later — deactivate without touching configuration
service.deactivatePayments();

// Re-activate with everything intact, nothing to reconfigure
service.activatePayments();

This means configuration can be set, reviewed, and approved before a feature goes live. It means deactivating during an incident is non-destructive — you don’t lose two weeks of configuration work. And it means a service doesn’t need to be fully implemented to have its API surface defined and guarded:

class AnalyticsService {
  trackEvent(event: TrackableEvent) {
    if (!this.isActive) return; // silent no-op when inactive
    // ...
  }

  generateReport(params: ReportParams) {
    if (!this.isActive) throw new FeatureNotActiveError('analytics');
    // ...
  }
}

The interface communicates intent. Activation communicates operational readiness.


The “Configure While Activating” Shorthand

The separation does introduce some verbosity — two calls where one might feel sufficient. For the common case where you’re configuring and activating at the same time, passing configuration to activate is a reasonable ergonomic shorthand:

// Full separation
service.configure({ threshold: 100, currency: 'CHF' });
service.activateFraudDetection();

// Shorthand — same outcome, less ceremony
service.activateFraudDetection({ threshold: 100, currency: 'CHF' });

The shorthand is a convenience, not a different philosophy. The underlying model still treats them as separate concerns — activation has a clear, explicit moment, and configuration remains independently retrievable regardless of activation state.


A Note on Naming: with* vs activate/deactivate

A common API convention for this kind of thing is with*withPayments(), withAnalytics(config), withDownload(). It reads fluently, especially in builder patterns.

The problem is that with* is ambiguous about what it’s actually doing. Is it configuring? Activating? Both? The word carries no operational signal. Two people reading the same withPayments() call can walk away with different mental models of what state the system is now in — is it configured? running? just registered?

activate and deactivate are explicit. They carry a lifecycle meaning. They make the call-site read like an operation, not a description. When you see service.activatePayments(), there’s no ambiguity — something is being switched on, deliberately. When you see service.deactivatePayments(), something is being switched off, deliberately, without side effects on configuration.

In larger systems where activation state affects behavior across multiple components or services, that clarity is part of the contract — not just a style preference.


The enabled: boolean Objection

A reasonable-looking alternative: put enabled: true in the config object. One place, one object.

The issue is that this makes activation a property of configuration rather than a first-class operation. Activation state becomes implicit — readable only by inspecting config, changeable only by mutating the same object that holds business-critical settings. There’s no clear API signal. There’s no lifecycle moment. There’s no explicit operation.

And in the frontend equivalent — [showDownload]="!!document?.downloadUrl" — the same problem appears. The component’s capability becomes derived from data rather than declared by context.

Activation deserves to be an explicit, visible part of the API surface.


In Short

Question
ConfigurationHow should this behave when running?
ActivationShould this be running right now, in this context?

Keep these questions separate and you get: non-destructive disabling, safe incremental delivery, clean activation lifecycles, and context-appropriate rendering without data manipulation. The occasional activateFeatureA(config) shorthand doesn’t compromise the model — it just reduces boilerplate for the common case.