flippercloud/flipper · error · Flipper::DuplicateGroup

Group #{name.inspect} has already been registered

Error message

Group #{name.inspect} has already been registered

What it means

Flipper keeps a process-global registry of groups (Flipper.groups_registry). Flipper.register(name, &block) inserts the group into that registry, and a second registration of the same name raises Flipper::DuplicateGroup instead of silently replacing the first block. This guards against two different match blocks both claiming one group name, which would make actor matching ambiguous. The registry only resets via Flipper.unregister_groups or process restart.

Source

Thrown at lib/flipper.rb:138

  #
  # name - The Symbol name of the group.
  # block - The block that should be used to determine if the group matches a
  #         given actor.
  #
  # Examples
  #
  #   Flipper.register(:admins) { |actor|
  #     actor.respond_to?(:admin?) && actor.admin?
  #   }
  #
  # Returns a Flipper::Group.
  # Raises Flipper::DuplicateGroup if the group is already registered.
  def register(name, &block)
    group = Types::Group.new(name, &block)
    groups_registry.add(group.name, group)
    group
  rescue Registry::DuplicateKey
    raise DuplicateGroup, "Group #{name.inspect} has already been registered"
  end

  # Public: Returns a Set of registered Types::Group instances.
  def groups
    groups_registry.values.to_set
  end

  # Public: Returns a Set of symbols where each symbol is a registered
  # group name. If you just want the names, this is more efficient than doing
  # `Flipper.groups.map(&:name)`.
  def group_names
    groups_registry.keys.to_set
  end

  # Public: Clears the group registry.
  #
  # Returns nothing.
  def unregister_groups

View on GitHub (pinned to 1f86de3ec9)

Solutions

  1. Register each group only once, and place registration in config/initializers/flipper.rb (runs once per process) rather than reloadable app code.
  2. If registration may run repeatedly (to_prepare, tests), guard it: Flipper.register(:admins) { ... } unless Flipper.group_exists?(:admins).
  3. In test suites, add Flipper.unregister_groups to a before/after hook so each example starts clean.
  4. If you genuinely must redefine a group at runtime, call Flipper.unregister_groups first, then re-register everything.

Example fix

// before (app/flipper_groups.rb — Zeitwerk reloads this in dev, second load raises)
Flipper.register(:admins) { |actor| actor.respond_to?(:admin?) && actor.admin? }

# after (config/initializers/flipper.rb — idempotent, runs once per process)
Rails.application.config.to_prepare do
  Flipper.register(:admins) { |actor| actor.respond_to?(:admin?) && actor.admin? } unless Flipper.group_exists?(:admins)
end
Defensive patterns

Strategy: validation

Validate before calling

Flipper.register(:admins) { |actor| actor.admin? } unless Flipper.group_exists?(:admins)

Try / catch

begin
  Flipper.register(:admins, &block)
rescue Flipper::DuplicateGroup
  # existing registration is authoritative; only safe if the block is identical
  raise if ENV["RAILS_ENV"] == "production"
end

Prevention

When it happens

Trigger: Calling Flipper.register(:admins) { ... } twice with the same Symbol in one process; calling Flipper.setup/initialization code that registers groups more than once; registering groups in a file that gets loaded twice (require + Zeitwerk autoload, or initializer re-run by Spring/rake tasks that reload initializers).

Common situations: Rails development reloading: groups registered in app/ code that Zeitwerk reloads, or in config.to_prepare blocks that fire on every code reload; registering the same group in both an initializer and a monkey patch/decorator; RSpec suites that call Flipper.register in a helper without unregistering between examples; engines/gems that each register the same group name.

Related errors


AI-assisted analysis of flippercloud/flipper@1f86de3ec9 (2026-08-23). Data as JSON: /api/errors/87601bc8d9381fe2. Report an issue: GitHub.