diff --git a/README.md b/README.md
index a92f0616a6..38587e93c8 100644
--- a/README.md
+++ b/README.md
@@ -40,3 +40,32 @@ If your shell reports that it cannot find the rspec binary, it may be necessary
 ## Usage
 
 Each directory contains a read me with instructions for the exercises within them.
+
+## Contributing
+
+Make sure that when changing an exercise or its tests that you also make the required changes to that exercise's solution under the `solutions/` directory.
+
+If you're writing an entirely new exercise, there's a script provided to help generate the correct directory structure and boilerplate. An example use for if you wanted to create a new exercise in `ruby_basics` for working with Ruby blocks:
+
+```bash
+bin/generate_exercise ruby_basics 13_blocks block_exercises
+```
+
+This will setup the following structure automatically:
+
+```bash
+.
+├── ruby_basics
+│   └── 13_blocks
+│       ├── exercises
+│       │   └── block_exercises.rb
+│       └── spec
+│           ├── block_exercises_spec.rb
+│           └── spec_helper.rb
+└── solutions
+    └── ruby_basics
+        └── 13_blocks
+            ├── exercises
+            └── spec
+                └── spec_helper.rb
+```
diff --git a/bin/generate_exercise b/bin/generate_exercise
new file mode 100755
index 0000000000..4768bb1ea2
--- /dev/null
+++ b/bin/generate_exercise
@@ -0,0 +1,5 @@
+#!/usr/bin/env ruby
+
+require_relative "../generators/exercise_generator"
+
+ExerciseGenerator.new(*ARGV).generate
diff --git a/generators/.rspec b/generators/.rspec
new file mode 100644
index 0000000000..c99d2e7396
--- /dev/null
+++ b/generators/.rspec
@@ -0,0 +1 @@
+--require spec_helper
diff --git a/generators/exercise_generator.rb b/generators/exercise_generator.rb
new file mode 100644
index 0000000000..6f0ae8633f
--- /dev/null
+++ b/generators/exercise_generator.rb
@@ -0,0 +1,59 @@
+require "fileutils"
+
+class ExerciseGenerator
+  def initialize(namespace, exercise_group_name, exercise_name)
+    @namespace = namespace
+    @exercise_group_name = exercise_group_name
+    @exercise_name = exercise_name
+  end
+
+  TEMPLATE_DIR_PATH = "generators/exercise_template".freeze
+
+  def generate
+    unless Dir.exist?(exercise_directory_path)
+      make_exercise_directory
+      make_solutions_directory
+    end
+
+    FileUtils.touch(exercise_file_path)
+    File.write(spec_file_path, spec_template)
+  end
+
+  private
+
+  attr_reader :namespace, :exercise_group_name, :exercise_name
+
+  def exercise_directory_path
+    File.join(namespace, exercise_group_name)
+  end
+
+  def exercise_file_path
+    File.join(exercise_directory_path, "exercises", "#{exercise_name}.rb")
+  end
+
+  def spec_file_path
+    File.join(exercise_directory_path, "spec", "#{exercise_name}_spec.rb")
+  end
+
+  def spec_template
+    <<~SPEC
+      require 'spec_helper'
+      require_relative '../exercises/#{exercise_name}'
+
+      RSpec.describe '' do
+      end
+    SPEC
+  end
+
+  def make_exercise_directory
+    FileUtils::mkdir_p(exercise_directory_path)
+    FileUtils::copy_entry(TEMPLATE_DIR_PATH, exercise_directory_path)
+  end
+
+  def make_solutions_directory
+    solutions_directory_path = File.join("solutions", exercise_directory_path)
+
+    FileUtils::mkdir_p(solutions_directory_path)
+    FileUtils::copy_entry(TEMPLATE_DIR_PATH, solutions_directory_path)
+  end
+end
diff --git a/generators/exercise_template/.rspec b/generators/exercise_template/.rspec
new file mode 100644
index 0000000000..279bfd9e99
--- /dev/null
+++ b/generators/exercise_template/.rspec
@@ -0,0 +1 @@
+--require spec_helper --format documentation --color
diff --git a/generators/exercise_template/spec/spec_helper.rb b/generators/exercise_template/spec/spec_helper.rb
new file mode 100644
index 0000000000..71b90cde10
--- /dev/null
+++ b/generators/exercise_template/spec/spec_helper.rb
@@ -0,0 +1,18 @@
+RSpec.configure do |config|
+  config.expect_with :rspec do |expectations|
+    expectations.include_chain_clauses_in_custom_matcher_descriptions = true
+  end
+
+  config.mock_with :rspec do |mocks|
+    mocks.verify_partial_doubles = true
+  end
+
+  config.shared_context_metadata_behavior = :apply_to_host_groups
+end
+
+module FormatterOverrides
+  def dump_pending(_)
+  end
+end
+
+RSpec::Core::Formatters::DocumentationFormatter.prepend FormatterOverrides
diff --git a/generators/spec/exercise_generator_spec.rb b/generators/spec/exercise_generator_spec.rb
new file mode 100644
index 0000000000..284173022f
--- /dev/null
+++ b/generators/spec/exercise_generator_spec.rb
@@ -0,0 +1,88 @@
+require_relative "../exercise_generator"
+
+describe ExerciseGenerator do
+  describe "#generate" do
+    subject(:generator) { described_class.new("test_namespace", "1_test_group", "test_exercise") }
+
+    before do
+      allow(FileUtils).to receive(:mkdir_p)
+      allow(FileUtils).to receive(:copy_entry)
+      allow(FileUtils).to receive(:touch)
+      allow(File).to receive(:write)
+    end
+
+    it "sends a `::touch` message to `FileUtils` to create the exercise file in the `exercises` subdir of the exercise directory" do
+      generator.generate
+
+      expected_filename = "test_namespace/1_test_group/exercises/test_exercise.rb"
+      expect(FileUtils).to have_received(:touch).with(expected_filename)
+    end
+
+    it "sends a `::write` message to `File` to create the spec file in the `spec` subdir of the exercise directory" do
+      generator.generate
+
+      expected_filename = "test_namespace/1_test_group/spec/test_exercise_spec.rb"
+      expected_content = <<~FILE_CONTENT
+        require 'spec_helper'
+        require_relative '../exercises/test_exercise'
+
+        RSpec.describe '' do
+        end
+      FILE_CONTENT
+
+      expect(File).to have_received(:write).with(expected_filename, expected_content)
+    end
+
+    context "when the directory structure does not exist for the exercise" do
+      before do
+        allow(Dir).to receive(:exist?).and_return(false)
+      end
+
+      it "sends a `::mkdir_p` message to `FileUtils` to create exercise path" do
+        generator.generate
+
+        expected_path = "test_namespace/1_test_group"
+        expect(FileUtils).to have_received(:mkdir_p).with(expected_path)
+      end
+
+      it "sends a `::copy_entry` message to `FileUtils` to copy boilerplate into exercise directory" do
+        generator.generate
+
+        expected_source = "generators/exercise_template"
+        expected_target = "test_namespace/1_test_group"
+        expect(FileUtils).to have_received(:copy_entry).with(expected_source, expected_target)
+      end
+
+      it "sends a `::mkdir_p` message to `FileUtils` to create solutions path" do
+        generator.generate
+
+        expected_path = "solutions/test_namespace/1_test_group"
+        expect(FileUtils).to have_received(:mkdir_p).with(expected_path)
+      end
+
+      it "sends a `::copy_entry` message to `FileUtils` to copy boilerplate into solutions directory" do
+        generator.generate
+
+        expected_source = "generators/exercise_template"
+        expected_target = "solutions/test_namespace/1_test_group"
+        expect(FileUtils).to have_received(:copy_entry).with(expected_source, expected_target)
+      end
+    end
+
+    context "when the directory structure already exists for exercise" do
+      before do
+        allow(Dir).to receive(:exist?).and_return(true)
+      end
+
+      it "does not send `::mkdir_p` message to `FileUtils`" do
+        generator.generate
+        expect(FileUtils).not_to have_received(:mkdir_p)
+      end
+
+      it "does not send a `::copy_entry` message to `FileUtils`" do
+        generator.generate
+        expect(FileUtils).not_to have_received(:copy_entry)
+      end
+    end
+  end
+end
diff --git a/generators/spec/spec_helper.rb b/generators/spec/spec_helper.rb
new file mode 100644
index 0000000000..c80d44b974
--- /dev/null
+++ b/generators/spec/spec_helper.rb
@@ -0,0 +1,98 @@
+# This file was generated by the `rspec --init` command. Conventionally, all
+# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
+# The generated `.rspec` file contains `--require spec_helper` which will cause
+# this file to always be loaded, without a need to explicitly require it in any
+# files.
+#
+# Given that it is always loaded, you are encouraged to keep this file as
+# light-weight as possible. Requiring heavyweight dependencies from this file
+# will add to the boot time of your test suite on EVERY test run, even for an
+# individual file that may not need all of that loaded. Instead, consider making
+# a separate helper file that requires the additional dependencies and performs
+# the additional setup, and require it from the spec files that actually need
+# it.
+#
+# See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
+RSpec.configure do |config|
+  # rspec-expectations config goes here. You can use an alternate
+  # assertion/expectation library such as wrong or the stdlib/minitest
+  # assertions if you prefer.
+  config.expect_with :rspec do |expectations|
+    # This option will default to `true` in RSpec 4. It makes the `description`
+    # and `failure_message` of custom matchers include text for helper methods
+    # defined using `chain`, e.g.:
+    #     be_bigger_than(2).and_smaller_than(4).description
+    #     # => "be bigger than 2 and smaller than 4"
+    # ...rather than:
+    #     # => "be bigger than 2"
+    expectations.include_chain_clauses_in_custom_matcher_descriptions = true
+  end
+
+  # rspec-mocks config goes here. You can use an alternate test double
+  # library (such as bogus or mocha) by changing the `mock_with` option here.
+  config.mock_with :rspec do |mocks|
+    # Prevents you from mocking or stubbing a method that does not exist on
+    # a real object. This is generally recommended, and will default to
+    # `true` in RSpec 4.
+    mocks.verify_partial_doubles = true
+  end
+
+  # This option will default to `:apply_to_host_groups` in RSpec 4 (and will
+  # have no way to turn it off -- the option exists only for backwards
+  # compatibility in RSpec 3). It causes shared context metadata to be
+  # inherited by the metadata hash of host groups and examples, rather than
+  # triggering implicit auto-inclusion in groups with matching metadata.
+  config.shared_context_metadata_behavior = :apply_to_host_groups
+
+# The settings below are suggested to provide a good initial experience
+# with RSpec, but feel free to customize to your heart's content.
+=begin
+  # This allows you to limit a spec run to individual examples or groups
+  # you care about by tagging them with `:focus` metadata. When nothing
+  # is tagged with `:focus`, all examples get run. RSpec also provides
+  # aliases for `it`, `describe`, and `context` that include `:focus`
+  # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
+  config.filter_run_when_matching :focus
+
+  # Allows RSpec to persist some state between runs in order to support
+  # the `--only-failures` and `--next-failure` CLI options. We recommend
+  # you configure your source control system to ignore this file.
+  config.example_status_persistence_file_path = "spec/examples.txt"
+
+  # Limits the available syntax to the non-monkey patched syntax that is
+  # recommended. For more details, see:
+  # https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/
+  config.disable_monkey_patching!
+
+  # This setting enables warnings. It's recommended, but in some cases may
+  # be too noisy due to issues in dependencies.
+  config.warnings = true
+
+  # Many RSpec users commonly either run the entire suite or an individual
+  # file, and it's useful to allow more verbose output when running an
+  # individual spec file.
+  if config.files_to_run.one?
+    # Use the documentation formatter for detailed output,
+    # unless a formatter has already been configured
+    # (e.g. via a command-line flag).
+    config.default_formatter = "doc"
+  end
+
+  # Print the 10 slowest examples and example groups at the
+  # end of the spec run, to help surface which specs are running
+  # particularly slow.
+  config.profile_examples = 10
+
+  # Run specs in random order to surface order dependencies. If you find an
+  # order dependency and want to debug it, you can fix the order by providing
+  # the seed, which is printed after each run.
+  #     --seed 1234
+  config.order = :random
+
+  # Seed global randomization in this process using the `--seed` CLI option.
+  # Setting this allows you to use `--seed` to deterministically reproduce
+  # test failures related to randomization by passing the same `--seed` value
+  # as the one that triggered the failure.
+  Kernel.srand config.seed
+=end
+end