diff --git a/README.md b/README.md
index bf9a536ae..4b9310dd9 100644
--- a/README.md
+++ b/README.md
@@ -818,6 +818,19 @@ SimpleCov.minimum_coverage_by_file line: 80
 SimpleCov.minimum_coverage_by_file line: 90, branch: 80
 ```
 
+### Minimum coverage by group
+
+You can define the minimum coverage percentage expected for specific groups. SimpleCov will return non-zero if unmet, 
+ensuring that coverage is consistent across different parts of your codebase.
+
+```ruby
+SimpleCov.minimum_coverage_by_group 'Models' => 80, 'Controllers' => 60
+# same as above (the default is to check line coverage)
+SimpleCov.minimum_coverage_by_group 'Models' => { line: 80 }, 'Controllers' => { line: 60 }
+# check for a minimum line and branch coverage for 'Models' and 'Controllers' groups
+SimpleCov.minimum_coverage_by_group 'Models' => { line: 90, branch: 80 }, 'Controllers' => { line: 60, branch: 50 }
+```
+
 ### Maximum coverage drop
 
 You can define the maximum coverage drop percentage at once. SimpleCov will return non-zero if exceeded.
diff --git a/lib/simplecov.rb b/lib/simplecov.rb
index f49c544a4..9fbfe27fa 100644
--- a/lib/simplecov.rb
+++ b/lib/simplecov.rb
@@ -252,11 +252,11 @@ def process_result(result)
     end
 
     # @api private
-    CoverageLimits = Struct.new(:minimum_coverage, :minimum_coverage_by_file, :maximum_coverage_drop, keyword_init: true)
+    CoverageLimits = Struct.new(:minimum_coverage, :minimum_coverage_by_file, :minimum_coverage_by_group, :maximum_coverage_drop, keyword_init: true)
     def result_exit_status(result)
       coverage_limits = CoverageLimits.new(
         minimum_coverage: minimum_coverage, minimum_coverage_by_file: minimum_coverage_by_file,
-        maximum_coverage_drop: maximum_coverage_drop
+        minimum_coverage_by_group: minimum_coverage_by_group, maximum_coverage_drop: maximum_coverage_drop
       )
 
       ExitCodes::ExitCodeHandling.call(result, coverage_limits: coverage_limits)
diff --git a/lib/simplecov/configuration.rb b/lib/simplecov/configuration.rb
index 128014f6e..f6fd04139 100644
--- a/lib/simplecov/configuration.rb
+++ b/lib/simplecov/configuration.rb
@@ -337,6 +337,25 @@ def minimum_coverage_by_file(coverage = nil)
       @minimum_coverage_by_file = coverage
     end
 
+    #
+    # Defines the minimum coverage per group required for the testsuite to pass.
+    # SimpleCov will return non-zero if the current coverage of the least covered group
+    # is below this threshold.
+    #
+    # Default is 0% (disabled)
+    #
+    def minimum_coverage_by_group(coverage = nil)
+      return @minimum_coverage_by_group ||= {} unless coverage
+
+      @minimum_coverage_by_group = coverage.dup.transform_values do |group_coverage|
+        group_coverage = {primary_coverage => group_coverage} if group_coverage.is_a?(Numeric)
+
+        raise_on_invalid_coverage(group_coverage, "minimum_coverage_by_group")
+
+        group_coverage
+      end
+    end
+
     #
     # Refuses any coverage drop. That is, coverage is only allowed to increase.
     # SimpleCov will return non-zero if the coverage decreases.
diff --git a/lib/simplecov/exit_codes.rb b/lib/simplecov/exit_codes.rb
index 3905ba8cb..774a33025 100644
--- a/lib/simplecov/exit_codes.rb
+++ b/lib/simplecov/exit_codes.rb
@@ -12,4 +12,5 @@ module ExitCodes
 require_relative "exit_codes/exit_code_handling"
 require_relative "exit_codes/maximum_coverage_drop_check"
 require_relative "exit_codes/minimum_coverage_by_file_check"
+require_relative "exit_codes/minimum_coverage_by_group_check"
 require_relative "exit_codes/minimum_overall_coverage_check"
diff --git a/lib/simplecov/exit_codes/exit_code_handling.rb b/lib/simplecov/exit_codes/exit_code_handling.rb
index eb564859d..a50064837 100644
--- a/lib/simplecov/exit_codes/exit_code_handling.rb
+++ b/lib/simplecov/exit_codes/exit_code_handling.rb
@@ -21,6 +21,7 @@ def coverage_checks(result, coverage_limits)
         [
           MinimumOverallCoverageCheck.new(result, coverage_limits.minimum_coverage),
           MinimumCoverageByFileCheck.new(result, coverage_limits.minimum_coverage_by_file),
+          MinimumCoverageByGroupCheck.new(result, coverage_limits.minimum_coverage_by_group),
           MaximumCoverageDropCheck.new(result, coverage_limits.maximum_coverage_drop)
         ]
       end
diff --git a/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb b/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb
new file mode 100644
index 000000000..fcee06fa9
--- /dev/null
+++ b/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+module SimpleCov
+  module ExitCodes
+    class MinimumCoverageByGroupCheck
+      def initialize(result, minimum_coverage_by_group)
+        @result = result
+        @minimum_coverage_by_group = minimum_coverage_by_group
+      end
+
+      def failing?
+        minimum_violations.any?
+      end
+
+      def report
+        minimum_violations.each do |violation|
+          $stderr.printf(
+            "%<criterion>s coverage by group %<group_name>s (%<covered>.2f%%) is below the expected minimum coverage (%<minimum_coverage>.2f%%).\n",
+            group_name: violation.fetch(:group_name),
+            covered: SimpleCov.round_coverage(violation.fetch(:actual)),
+            minimum_coverage: violation.fetch(:minimum_expected),
+            criterion: violation.fetch(:criterion).capitalize
+          )
+        end
+      end
+
+      def exit_code
+        SimpleCov::ExitCodes::MINIMUM_COVERAGE
+      end
+
+    private
+
+      attr_reader :result, :minimum_coverage_by_group
+
+      def minimum_violations
+        @minimum_violations ||=
+          compute_minimum_coverage_data.select do |achieved|
+            achieved.fetch(:actual) < achieved.fetch(:minimum_expected)
+          end
+      end
+
+      def compute_minimum_coverage_data
+        minimum_coverage_data = []
+
+        minimum_coverage_by_group.each do |group_name, minimum_group_coverage|
+          minimum_group_coverage.each do |criterion, expected_percent|
+            actual_coverage = result.groups.fetch(group_name).coverage_statistics.fetch(criterion)
+            minimum_coverage_data << minimum_coverage_hash(group_name, criterion, expected_percent, SimpleCov.round_coverage(actual_coverage.percent))
+          end
+        end
+
+        minimum_coverage_data
+      end
+
+      def minimum_coverage_hash(group_name, criterion, minimum_expected, actual)
+        {
+          group_name: group_name,
+          criterion: criterion,
+          minimum_expected: minimum_expected,
+          actual: actual
+        }
+      end
+    end
+  end
+end
diff --git a/spec/configuration_spec.rb b/spec/configuration_spec.rb
index 7cae394e8..20bed6cf4 100644
--- a/spec/configuration_spec.rb
+++ b/spec/configuration_spec.rb
@@ -124,6 +124,73 @@
       it_behaves_like "setting coverage expectations", :minimum_coverage_by_file
     end
 
+    describe "#minimum_coverage_by_group" do
+      after do
+        config.clear_coverage_criteria
+      end
+
+      it "does not warn you about your usage" do
+        expect(config).not_to receive(:warn)
+        config.minimum_coverage_by_group({"Test Group 1" => 100.00})
+      end
+
+      it "warns you about your usage" do
+        expect(config).to receive(:warn).with("The coverage you set for minimum_coverage_by_group is greater than 100%")
+        config.minimum_coverage_by_group({"Test Group 1" => 100.01})
+      end
+
+      it "sets the right coverage value when called with a number" do
+        config.minimum_coverage_by_group({"Test Group 1" => 80})
+
+        expect(config.minimum_coverage_by_group).to eq({"Test Group 1" => {line: 80}})
+      end
+
+      it "sets the right coverage when called with a hash of just line" do
+        config.minimum_coverage_by_group({"Test Group 1" => {line: 85.0}})
+
+        expect(config.minimum_coverage_by_group).to eq({"Test Group 1" => {line: 85.0}})
+      end
+
+      it "sets the right coverage when called with a hash of just branch" do
+        config.enable_coverage :branch
+        config.minimum_coverage_by_group({"Test Group 1" => {branch: 85.0}})
+
+        expect(config.minimum_coverage_by_group).to eq({"Test Group 1" => {branch: 85.0}})
+      end
+
+      it "sets the right coverage when called with both line and branch" do
+        config.enable_coverage :branch
+        config.minimum_coverage_by_group({"Test Group 1" => {branch: 85.0, line: 95.4}})
+
+        expect(config.minimum_coverage_by_group).to eq({"Test Group 1" => {branch: 85.0, line: 95.4}})
+      end
+
+      it "raises when trying to set branch coverage but not enabled" do
+        expect do
+          config.minimum_coverage_by_group({"Test Group 1" => {branch: 42}})
+        end.to raise_error(/branch.*disabled/i)
+      end
+
+      it "raises when unknown coverage criteria provided" do
+        expect do
+          config.minimum_coverage_by_group({"Test Group 1" => {unknown: 42}})
+        end.to raise_error(/unsupported.*unknown/i)
+      end
+
+      context "when primary coverage is set" do
+        before do
+          config.enable_coverage :branch
+          config.primary_coverage :branch
+        end
+
+        it "sets the right coverage value when called with a number" do
+          config.minimum_coverage_by_group({"Test Group 1" => 80})
+
+          expect(config.minimum_coverage_by_group).to eq({"Test Group 1" => {branch: 80}})
+        end
+      end
+    end
+
     describe "#maximum_coverage_drop" do
       it_behaves_like "setting coverage expectations", :maximum_coverage_drop
     end
diff --git a/spec/exit_codes/minimum_coverage_by_group_check_spec.rb b/spec/exit_codes/minimum_coverage_by_group_check_spec.rb
new file mode 100644
index 000000000..3285de424
--- /dev/null
+++ b/spec/exit_codes/minimum_coverage_by_group_check_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require "helper"
+
+RSpec.describe SimpleCov::ExitCodes::MinimumCoverageByGroupCheck do
+  subject { described_class.new(result, minimum_coverage_by_group) }
+
+  let(:coverage_statistics) { {line: SimpleCov::CoverageStatistics.new(covered: 8, missed: 2), branch: SimpleCov::CoverageStatistics.new(covered: 8, missed: 2)} }
+  let(:result) { instance_double(SimpleCov::Result, groups: {"Test Group 1" => instance_double(SimpleCov::FileList, coverage_statistics: coverage_statistics)}) }
+  let(:stats) { {"Test Group 1" => coverage_statistics} }
+
+  context "everything exactly ok" do
+    let(:minimum_coverage_by_group) { {"Test Group 1" => {line: 80.0}} }
+
+    it { is_expected.not_to be_failing }
+  end
+
+  context "coverage violated" do
+    let(:minimum_coverage_by_group) { {"Test Group 1" => {line: 90.0}} }
+
+    it { is_expected.to be_failing }
+  end
+
+  context "coverage slightly violated" do
+    let(:minimum_coverage_by_group) { {"Test Group 1" => {line: 80.01}} }
+
+    it { is_expected.to be_failing }
+  end
+
+  context "one criterion violated" do
+    let(:minimum_coverage_by_group) { {"Test Group 1" => {line: 80.0, branch: 90.0}} }
+
+    it { is_expected.to be_failing }
+  end
+end