diff --git a/2021/README.md b/2021/README.md
index 8b64c0baa1b7c8e54080afb22bfa4f38ae64ca70..52bb9049992e5bbf6f451dce949fc549071cdbf1 100644
--- a/2021/README.md
+++ b/2021/README.md
@@ -56,7 +56,7 @@ Part 2 looks a bit more interesting.  We *could* go for an
 O(num_crabs * max_distance^2) solution, or we could recognize what Euler
 recognized: ∑_{i=1}^{n}i = n(n+1)/2.
 
-### Day 8
+## Day 8
 
 Seven-segment displays are cool. This has the potential to be a pain. Part 1
 looks okay -- examining only the output patterns, count the number of patterns
@@ -85,3 +85,32 @@ explanation, I'm going to use numerals to depict the sets of illuminated segment
 It turns out we can ascertain the digits without needing to record the segments.
 That's nifty.
 
+## Day 9
+
+Part 1 doesn't look too fancy I'll simplify the adjacency-check a little by
+making the heightmap larger by two in each dimension so that I can create a
+"frame" of `MAX_VALUE`s.
+
+Hmmm -- While that O(num_rows * num_columns) algorithm took a fraction of a 
+second, it was noticeable on the human scale. I don't think there's a better
+big-O approach, but the constant factors could be in play if I were to look
+for optimizations.
+
+Part 2 defines "basin" okay, but requires a little thought to express as
+numbers. In the examples, the basins seem to be monotonic until reaching
+height 9. Monotonic makes sense, given the definition "...flow downward to a
+single low point," but what about a location of height 6 that has a basin on
+either side?
+- I'm going to assume, for now, that there are locations whose height is less
+  than 9 that are between two basins -- the simple check to see if there's an
+  adjacent location of greater height potentially could count a location as
+  being in two locations otherwise. If I'm wrong, I'll fix it.
+  - Come to think of it, plateaus would also be a problem for the simple
+    check. The examples don't have any plateaus.
+- I'm going to change my part 1 code to create a height-9 frame. That's
+  enough for the low-point calculation and will simply part 2 a little. I'm
+  also going to change my part 1 code to record the low points so that I
+  don't have to re-calculate them for part 2.
+- We'll go for a finite element approach.
+
+Hmm. Part 2 takes a few seconds, but no surprise there -- it's now O(n^5).
diff --git a/2021/src/main/java/edu/unl/cse/bohn/year2021/Day9.java b/2021/src/main/java/edu/unl/cse/bohn/year2021/Day9.java
new file mode 100644
index 0000000000000000000000000000000000000000..5204255d4912602d111e8f4ba1447194612bc764
--- /dev/null
+++ b/2021/src/main/java/edu/unl/cse/bohn/year2021/Day9.java
@@ -0,0 +1,101 @@
+package edu.unl.cse.bohn.year2021;
+
+import edu.unl.cse.bohn.Puzzle;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+@SuppressWarnings("unused")
+public class Day9 extends Puzzle {
+    public record Point(int x, int y) {
+    }
+
+    private long[][] heightmap;
+    private boolean[][] lowPoints;
+
+    public Day9(boolean isProductionReady) {
+        super(isProductionReady);
+        sampleData = """
+                2199943210
+                3987894921
+                9856789892
+                8767896789
+                9899965678""";
+    }
+
+    @Override
+    public long computePart1(List<String> data) {
+        heightmap = new long[data.size() + 2][data.get(0).length() + 2];
+        lowPoints = new boolean[data.size() + 2][data.get(0).length() + 2];
+        for (long[] row : heightmap) {
+            Arrays.fill(row, 9L);
+        }
+        for (boolean[] row : lowPoints) {
+            Arrays.fill(row, false);
+        }
+        for (int i = 0; i < data.size(); i++) {
+            String line = data.get(i);
+            for (int j = 0; j < line.length(); j++) {
+                heightmap[i + 1][j + 1] = Long.parseLong(String.valueOf(line.charAt(j)));
+            }
+        }
+        long riskTotal = 0L;
+        for (int i = 1; i < heightmap.length - 1; i++) {
+            for (int j = 1; j < heightmap[i].length - 1; j++) {
+                long height = heightmap[i][j];
+                boolean isLowPoint = height < heightmap[i - 1][j] &&
+                        height < heightmap[i + 1][j] &&
+                        height < heightmap[i][j - 1] &&
+                        height < heightmap[i][j + 1];
+                long risk = height + 1;
+                if (isLowPoint) {
+                    lowPoints[i][j] = true;
+                    riskTotal += risk;
+                }
+            }
+        }
+        return riskTotal;
+    }
+
+    @Override
+    public long computePart2(List<String> data) {
+        List<Integer> basinSizes = new LinkedList<>();
+        for (int i = 0; i < heightmap.length; i++) {
+            for (int j = 0; j < heightmap[i].length; j++) {
+                if (lowPoints[i][j]) {
+                    Set<Point> basinPoints = new HashSet<>();
+                    basinPoints.add(new Point(i, j));
+                    boolean newPointsAdded = true;
+                    while (newPointsAdded) {
+                        newPointsAdded = false;
+                        for (int m = 0; m < heightmap.length; m++) {
+                            for (int n = 0; n < heightmap[m].length; n++) {
+                                long height = heightmap[m][n];
+                                Point point = new Point(m, n);
+                                if (!basinPoints.contains(point) && height < 9) {
+                                    for (Point neighbor : Set.of(new Point(m - 1, n), new Point(m + 1, n),
+                                            new Point(m, n - 1), new Point(m, n + 1))) {
+                                        if (basinPoints.contains(neighbor) &&
+                                                height > heightmap[neighbor.x()][neighbor.y()]) {
+                                            basinPoints.add(point);
+                                            newPointsAdded = true;
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                    basinSizes.add(basinPoints.size());
+                }
+            }
+        }
+        basinSizes.sort(null);
+        int numberOfBasins = basinSizes.size();
+        return (long)basinSizes.get(numberOfBasins - 1) *
+                (long)basinSizes.get(numberOfBasins - 2) *
+                (long)basinSizes.get(numberOfBasins - 3);
+    }
+}