Prompt
ChatGPT: We have a Rails backend, React frontend, and a few microservices. How should we introduce feature flags without making the system messy?
Latest observation
A good way to introduce feature flags in a Rails + React + microservices stack is to treat them as a product/platform capability, not just a bunch of if statements sprinkled through the code.
Here’s a practical approach that keeps things clean.
1) Pick one source of truth for flag evaluation
Use a centralized flag service or platform if you can:
- LaunchDarkly
- Unleash
- Split
- ConfigCat
- custom service, if you already have strong platform support
Why:
- Consistent rollout logic across Rails, React, and microservices
- Auditability
- Targeting by user/account/org/environment
- Easy gradual rollout and rollback
If you build it yourself, keep the interface consistent:
flag_enabled?(flag_name, actor/context)- same semantics everywhere
2) Decide what types of flags you allow
Not all flags should be treated the same. Define categories early:
- Release flags: hide unfinished features
- Ops flags: kill switches, degrade functionality
- Experiment flags: A/B tests
- Permission flags: feature entitlements by plan/customer
- Migration flags: old/new data paths
This matters because each type has a different lifecycle and cleanup policy.
3) Keep flag logic at the edges, not deep in business logic
A common way systems get messy is when flags penetrate core domain code everywhere.
Better pattern:
- Determine behavior in a thin composition layer
- Pass the chosen strategy into services
- Keep business logic free of “flag checking” if possible
Example:
- Bad:
if feature_enabled?(:new_pricing) ...inside 8 different model methods - Better: resolve once in controller/service and call
PricingCalculatorV2orPricingCalculatorV1
In other words: feature flag chooses implementation; implementation stays flag-free.
4) Establish a shared flag context model
To evaluate flags consistently, define a common context shape across the stack:
- user ID
- account/org ID
- role/plan
- environment
- request ID
- service name
- locale/region if relevant
Rails should build this context on the server. React should receive only what it needs. Microservices should get enough identity/context via request headers or a signed token, or evaluate server-side using the same identifiers.
Important: don’t let every service invent its own “user context” definition.
5) Prefer server-side evaluation when possible
For sensitive or business-critical flags:
- evaluate in Rails or a backend service
- send the result to React as part of page bootstrap/API response
Reasons:
- avoids exposing internal targeting rules
- keeps frontend simple
- prevents UI/backend divergence
For purely UI flags, client-side evaluation is fine if the flag values are safe to expose.
A good rule:
- Business logic flags: backend
- Presentation flags: frontend
- Cross-cutting release flags: backend-owned, optionally mirrored to frontend
6) Cache carefully, but don’t make flags stale for too long
Flags should be fast, but you still need near-real-time changes for rollout and kill switches.
Recommended:
- cache flag config in-memory with short TTL
- use streaming/webhooks/pub-sub if your provider supports it
- avoid per-request network calls to the flag provider in every service if latency matters
For microservices, a common pattern is:
- load flag config on startup
- refresh periodically
- support forced refresh on config change
For kill switches, choose a path that updates quickly.
7) Build a wrapper in each codebase, not raw vendor calls everywhere
Don’t scatter vendor SDK calls through the app.
Create a local abstraction in each stack:
FeatureFlags.enabled?(:flag_name, context)featureFlags.isEnabled("flagName", context)
Benefits:
- one place for defaults, analytics, logging, fallbacks
- easier provider migration
- easier testing/mocking
- consistent naming and semantics
Then keep vendor-specific details hidden behind that wrapper.
8) Make flags observable
If you introduce flags, you need visibility into:
- who is seeing what
- how often a flag is used
- whether a rollout is causing errors
- whether flags are stale
Track:
- flag evaluations
- variant chosen
- errors correlated with flag state
- rollout percentages
- last modified date / owner / expiry
This is especially useful for debugging “works for me” issues in multi-service environments.
9) Enforce ownership and expiration
A major source of mess is abandoned flags.
Every flag should have:
- owner
- purpose
- creation date
- expiry/removal date
- cleanup ticket or task
Make it part of your engineering process:
- PR template asks “Is this behind a flag?”
- flag registry includes owner and expiry
- automated reminders for stale flags
A rule of thumb:
- release flags should be temporary
- only long-lived flags should be true product entitlements or permanent ops controls
10) Keep frontend and backend in sync explicitly
With Rails + React, make sure the frontend does not guess at backend behavior.
Good patterns:
- backend returns a
featureFlagsobject in bootstrap payload - backend includes flag state in API responses when needed
- frontend only uses flags for UI branching, not source-of-truth business decisions
Avoid:
- frontend enabling a feature the backend doesn’t support yet
- duplicate flag names with different meanings in client and server
11) For microservices, minimize distributed flag logic
Microservices can make flags messy if every service independently evaluates everything.
Better options:
-
Evaluate at the edge/orchestrator
- Rails or API gateway decides which flow to use
- downstream services receive already-decided behavior
-
Evaluate in each service only when needed
- for service-local behavior or service-owned migration flags
-
Use shared context and shared naming
- if multiple services must evaluate the same flag, they should do so consistently
Avoid a setup where service A thinks a feature is on and service B thinks it’s off because they read different contexts or different versions of the flag config.
12) Define clear rules for testing
Testing with flags can become chaotic unless you standardize it.
Have a few standard test patterns:
- default-off tests
- default-on tests
- variant-specific tests
- contract tests for frontend/backend alignment
- end-to-end tests for rollout-critical flags
In unit tests:
- inject flag values via wrapper/mocks
- don’t hit the real flag service
In integration tests:
- set explicit flag fixtures
- test both old and new paths when flags gate behavior
13) Plan for cleanup from day one
The cleanest flag systems are the ones that aggressively remove flags.
When a rollout completes:
- remove dead code
- remove stale config
- delete unused UI branches
- remove metrics/logging tied to old paths
- close the cleanup ticket
If a flag becomes permanent, rename and document it as a real capability/entitlement rather than leaving it as “temporary.”
A simple architecture that usually works well
Rails
- owns server-side evaluation for business flags
- exposes flag state to React on boot/API
- uses a local
FeatureFlagswrapper
React
- consumes flag state from Rails or a client SDK
- only uses flags for UI/presentation branching
- no business-critical decisions
Microservices
- use the same flag platform/wrapper
- evaluate only for service-local behavior
- prefer shared context and consistent flag naming
- avoid calling the flag provider on every request unless necessary
A few rules of thumb
- One flag system, one naming convention
- Backend decides business logic
- Frontend decides presentation
- Abstract vendor SDKs
- Track ownership and expiry
- Remove flags quickly
- Prefer strategies over nested
ifblocks - Keep evaluation consistent across services
If you want, I can also propose:
- a concrete folder/module structure for Rails + React + microservices, or
- a rollout plan for introducing flags incrementally without a big rewrite.