diff --git a/2021/lib/advent_of_code.ex b/2021/lib/advent_of_code.ex
index 33601c2..6b37c41 100644
--- a/2021/lib/advent_of_code.ex
+++ b/2021/lib/advent_of_code.ex
@@ -2,17 +2,4 @@ defmodule AdventOfCode do
   @moduledoc """
   Documentation for `AdventOfCode`.
   """
-
-  @doc """
-  Hello world.
-
-  ## Examples
-
-      iex> AdventOfCode.hello()
-      :world
-
-  """
-  def hello do
-    :world
-  end
 end
diff --git a/2021/lib/advent_of_code/puzzle_solver.ex b/2021/lib/advent_of_code/puzzle_solver.ex
new file mode 100644
index 0000000..f79f334
--- /dev/null
+++ b/2021/lib/advent_of_code/puzzle_solver.ex
@@ -0,0 +1,16 @@
+defmodule AdventOfCode.PuzzleSolver do
+  @moduledoc """
+  Behaviour for a puzzle solution.
+  """
+
+  @doc """
+  Given the input as a stream, return the solution as a string
+  """
+  @callback solve(IO.Stream.t()) :: String.t()
+
+  defmacro __using__(_) do
+    quote do
+      @behaviour AdventOfCode.PuzzleSolver
+    end
+  end
+end
diff --git a/2021/test/advent_of_code_test.exs b/2021/test/advent_of_code_test.exs
deleted file mode 100644
index 8a98262..0000000
--- a/2021/test/advent_of_code_test.exs
+++ /dev/null
@@ -1,8 +0,0 @@
-defmodule AdventOfCodeTest do
-  use ExUnit.Case
-  doctest AdventOfCode
-
-  test "greets the world" do
-    assert AdventOfCode.hello() == :world
-  end
-end
diff --git a/2021/test/support/puzzle_case.ex b/2021/test/support/puzzle_case.ex
new file mode 100644
index 0000000..3d51249
--- /dev/null
+++ b/2021/test/support/puzzle_case.ex
@@ -0,0 +1,18 @@
+defmodule AdventOfCode.PuzzleCase do
+  @moduledoc """
+  Defines tests for an `AdventOfCode.PuzzleSolver` module.
+  """
+
+  use ExUnit.CaseTemplate
+
+  using module: module do
+    quote bind_quoted: [module: module] do
+      @module module
+
+      defp assert_solution(input, desired_output) do
+        actual_output = @module.run(input)
+        assert actual_output == desired_output
+      end
+    end
+  end
+end