diff --git a/README.md b/README.md
index 11dea01..b382bbb 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,7 @@ article; each module's own README has that article's version table, quickstart,
| [`synchronizers`](synchronizers/) | CountDownLatch vs CyclicBarrier vs Phaser vs Semaphore in Java |
| [`executors`](executors/) | ExecutorService Done Right: Shutdown, try-with-resources and Virtual Thread Executors |
| [`concurrency-interview`](concurrency-interview/) | Top 40 Java Concurrency Interview Questions and Answers (2026) |
+| [`arithmetic`](arithmetic/) | Add Two Numbers in Java Without Overflow: addExact, Widening, and BigInteger |
## License
diff --git a/arithmetic/README.md b/arithmetic/README.md
new file mode 100644
index 0000000..c5b7737
--- /dev/null
+++ b/arithmetic/README.md
@@ -0,0 +1,47 @@
+# arithmetic
+
+Companion code for the ankurm.com post *"Add Two Numbers in Java Without Overflow."* Eighth
+module in `java-core-examples`.
+
+`a + b` on two Java `int`s never throws when the true mathematical result doesn't fit - it wraps
+around silently, two's-complement style. This module reproduces that wraparound with real
+numbers, then works through every real fix: the `Math` class's `*Exact()` family (which throws
+`ArithmeticException` instead of wrapping), widening to `long`, `BigInteger` for unbounded
+precision, and the actual bit trick `Math.addExact` uses internally to detect overflow without
+doing the addition twice at different widths.
+
+## Versions this was built and tested against
+
+| Component | Version | Notes |
+|---|---|---|
+| JDK | 25.0.4.1+1 (Temurin, LTS) | No preview flags needed - every API used here is finalized. |
+| JUnit Jupiter | 5.11.0 | Includes a 100,000-iteration randomized cross-check per repeat. |
+| Maven | 3.9.11 | |
+| Hardware | 2 vCPU x86-64 VM | Same sandbox as the rest of this series. |
+
+## Quickstart
+
+```bash
+export JAVA_HOME=/path/to/jdk-25
+mvn package
+java -cp target/classes com.ankurm.arithmetic.OverflowWrapDemo
+java -cp target/classes com.ankurm.arithmetic.ExactArithmeticDemo
+```
+
+`scripts/run-all.sh` regenerates every file in `output/` (needs `JDK25_HOME`).
+
+## What's in here
+
+| File | What it shows |
+|---|---|
+| `.../OverflowWrapDemo.java` | `int` and `long` addition and multiplication wrapping silently past their max values - no exception, just a wrong answer. |
+| `.../ExactArithmeticDemo.java` | The `Math` `*Exact()` family: `addExact`, `subtractExact`, `multiplyExact`, `incrementExact`, `decrementExact`, `negateExact`, `toIntExact`, `absExact` - normal case and overflow-throws case for each, plus the `Math.abs(Integer.MIN_VALUE)` trap that `absExact` exists to catch. |
+| `.../WideningAndBigIntegerDemo.java` | Widening to `long` fixes the `int`-range overflow (but `long` can still overflow); `BigInteger` never overflows at all. |
+| `.../OverflowBitTrickDemo.java` | The XOR-based overflow test for addition, cross-checked against `Math.addExact()` on the same inputs. |
+| `src/test/.../ArithmeticClaimsTest.java` | Asserts every claim above, including a 100,000-sample-per-repeat randomized cross-check of the bit-trick detector against `BigInteger` ground truth. |
+| `output/01`-`04` | Each demo's real run. |
+| `output/05` | JUnit correctness run. |
+
+## License
+
+MIT - see the [repo-wide LICENSE](../LICENSE).
diff --git a/arithmetic/output/01-overflow-wrap.txt b/arithmetic/output/01-overflow-wrap.txt
new file mode 100644
index 0000000..6038b2f
--- /dev/null
+++ b/arithmetic/output/01-overflow-wrap.txt
@@ -0,0 +1,16 @@
+$ java OverflowWrapDemo
+
+=== int addition wraps silently past Integer.MAX_VALUE ===
+Integer.MAX_VALUE = 2147483647
+Integer.MAX_VALUE + 1 = -2147483648 (wrapped to Integer.MIN_VALUE, no exception)
+Integer.MIN_VALUE = -2147483648
+
+=== A realistic case: summing item prices in cents overflows int ===
+priceCents * quantity = 1800000000 * 2 = -694967296 (should be 3,600,000,000 - this is wrong, and no exception was thrown)
+
+=== Multiplication overflows just as silently as addition ===
+100000 * 100000 = 1410065408 (true value is 10,000,000,000)
+
+=== long has the same problem, just a much higher ceiling ===
+Long.MAX_VALUE = 9223372036854775807
+Long.MAX_VALUE + 1 = -9223372036854775808 (wrapped to Long.MIN_VALUE)
diff --git a/arithmetic/output/02-exact-arithmetic.txt b/arithmetic/output/02-exact-arithmetic.txt
new file mode 100644
index 0000000..cdc77dc
--- /dev/null
+++ b/arithmetic/output/02-exact-arithmetic.txt
@@ -0,0 +1,24 @@
+$ java ExactArithmeticDemo
+
+=== addExact / subtractExact / multiplyExact: normal case ===
+Math.addExact(20, 22) = 42
+Math.subtractExact(50, 8) = 42
+Math.multiplyExact(6, 7) = 42
+
+=== addExact / multiplyExact: overflow case - throws instead of wrapping ===
+Math.addExact(Integer.MAX_VALUE, 1) threw: integer overflow
+Math.multiplyExact(100000, 100000) threw: integer overflow
+
+=== incrementExact / decrementExact / negateExact ===
+Math.incrementExact(41) = 42
+Math.decrementExact(43) = 42
+Math.negateExact(-42) = 42
+Math.incrementExact(Integer.MAX_VALUE) threw: integer overflow
+
+=== toIntExact: the safe narrowing cast from long back to int ===
+Math.toIntExact(2000000000L) = 2000000000
+Math.toIntExact(3000000000L) threw: integer overflow
+
+=== The classic trap: Math.abs(Integer.MIN_VALUE) does NOT throw, and is wrong ===
+Math.abs(Integer.MIN_VALUE) = -2147483648 (still negative! +2147483648 doesn't fit in an int)
+Math.absExact(Integer.MIN_VALUE) threw: Overflow to represent absolute value of Integer.MIN_VALUE
diff --git a/arithmetic/output/03-widening-and-biginteger.txt b/arithmetic/output/03-widening-and-biginteger.txt
new file mode 100644
index 0000000..26e41a0
--- /dev/null
+++ b/arithmetic/output/03-widening-and-biginteger.txt
@@ -0,0 +1,13 @@
+$ java WideningAndBigIntegerDemo
+
+=== Widening to long fixes the int-range overflow from OverflowWrapDemo ===
+int: Integer.MAX_VALUE + 1 = -2147483648 (wrong)
+long: (long) Integer.MAX_VALUE + 1 = 2147483648 (correct)
+
+=== Widening only raises the ceiling - long still overflows eventually ===
+Long.MAX_VALUE + 1 = -9223372036854775808 (wrapped to Long.MIN_VALUE - same bug, bigger numbers)
+
+=== BigInteger never overflows - it grows to fit the true value ===
+BigInteger.valueOf(Long.MAX_VALUE).add(ONE) = 9223372036854775808
+BigInteger.valueOf(2).pow(100) = 1267650600228229401496703205376
+(that value has no exact long or int representation at all)
diff --git a/arithmetic/output/04-overflow-bit-trick.txt b/arithmetic/output/04-overflow-bit-trick.txt
new file mode 100644
index 0000000..2d47bbe
--- /dev/null
+++ b/arithmetic/output/04-overflow-bit-trick.txt
@@ -0,0 +1,15 @@
+$ java OverflowBitTrickDemo
+
+=== Hand-rolled overflow detector vs. known cases ===
+2000000000 + 2000000000 -> raw int result -294967296, overflow detected: true
+2000000000 + -1000000000 -> raw int result 1000000000, overflow detected: false
+2147483647 + 1 -> raw int result -2147483648, overflow detected: true
+-2147483648 + -1 -> raw int result 2147483647, overflow detected: true
+100 + 200 -> raw int result 300, overflow detected: false
+
+=== Cross-checked against Math.addExact() on the same inputs ===
+2000000000 + 2000000000: bit-trick says overflow=true, Math.addExact() agrees=true
+2000000000 + -1000000000: bit-trick says overflow=false, Math.addExact() agrees=true
+2147483647 + 1: bit-trick says overflow=true, Math.addExact() agrees=true
+-2147483648 + -1: bit-trick says overflow=true, Math.addExact() agrees=true
+100 + 200: bit-trick says overflow=false, Math.addExact() agrees=true
diff --git a/arithmetic/output/05-correctness-tests.txt b/arithmetic/output/05-correctness-tests.txt
new file mode 100644
index 0000000..a20b176
--- /dev/null
+++ b/arithmetic/output/05-correctness-tests.txt
@@ -0,0 +1,4 @@
+-------------------------------------------------------------------------------
+Test set: com.ankurm.arithmetic.ArithmeticClaimsTest
+-------------------------------------------------------------------------------
+Tests run: 14, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.438 s -- in com.ankurm.arithmetic.ArithmeticClaimsTest
diff --git a/arithmetic/pom.xml b/arithmetic/pom.xml
new file mode 100644
index 0000000..b01b7d2
--- /dev/null
+++ b/arithmetic/pom.xml
@@ -0,0 +1,43 @@
+
+
Expressed without any branching on sign:
+ * {@code overflow = ((x ^ result) & (y ^ result)) < 0}. XOR-ing two ints and checking the sign
+ * bit of the result is a cheap way to ask "do these differ in sign?" - the expression is true
+ * (negative, i.e. the top bit is set) exactly when both {@code x} and {@code y} differ in sign
+ * from {@code result}, which is exactly the overflow condition above. This is not a novel trick -
+ * it is functionally the same check the JDK's own {@code Math.addExact} source uses internally.
+ */
+public class OverflowBitTrickDemo {
+
+ /** Returns true if x + y overflows the int range, computed without widening to long. */
+ static boolean additionOverflows(int x, int y) {
+ int result = x + y; // allowed to wrap; we only use it to test the sign relationship
+ return ((x ^ result) & (y ^ result)) < 0;
+ }
+
+ public static void main(String[] args) {
+ System.out.println("=== Hand-rolled overflow detector vs. known cases ===");
+ report(2_000_000_000, 2_000_000_000); // same sign, overflows
+ report(2_000_000_000, -1_000_000_000); // different signs, never overflows
+ report(Integer.MAX_VALUE, 1); // classic edge case
+ report(Integer.MIN_VALUE, -1); // negative-side overflow
+ report(100, 200); // ordinary case, no overflow
+
+ System.out.println();
+ System.out.println("=== Cross-checked against Math.addExact() on the same inputs ===");
+ int[][] cases = {
+ {2_000_000_000, 2_000_000_000},
+ {2_000_000_000, -1_000_000_000},
+ {Integer.MAX_VALUE, 1},
+ {Integer.MIN_VALUE, -1},
+ {100, 200}
+ };
+ for (int[] c : cases) {
+ boolean detected = additionOverflows(c[0], c[1]);
+ boolean addExactThrew;
+ try {
+ Math.addExact(c[0], c[1]);
+ addExactThrew = false;
+ } catch (ArithmeticException e) {
+ addExactThrew = true;
+ }
+ System.out.println(c[0] + " + " + c[1] + ": bit-trick says overflow=" + detected
+ + ", Math.addExact() agrees=" + (detected == addExactThrew));
+ }
+ }
+
+ private static void report(int x, int y) {
+ boolean overflow = additionOverflows(x, y);
+ System.out.println(x + " + " + y + " -> raw int result " + (x + y) + ", overflow detected: " + overflow);
+ }
+}
diff --git a/arithmetic/src/main/java/com/ankurm/arithmetic/OverflowWrapDemo.java b/arithmetic/src/main/java/com/ankurm/arithmetic/OverflowWrapDemo.java
new file mode 100644
index 0000000..e291fc6
--- /dev/null
+++ b/arithmetic/src/main/java/com/ankurm/arithmetic/OverflowWrapDemo.java
@@ -0,0 +1,42 @@
+package com.ankurm.arithmetic;
+
+/**
+ * Java's {@code int} and {@code long} arithmetic never throws on overflow. It wraps around
+ * silently, two's-complement style, and the compiler and JVM both consider {@code a + b} on
+ * two ints to be perfectly well-defined behavior even when the mathematically correct result
+ * doesn't fit. This demo reproduces that wraparound with real numbers instead of just quoting
+ * {@code Integer.MAX_VALUE}.
+ */
+public class OverflowWrapDemo {
+
+ public static void main(String[] args) {
+ System.out.println("=== int addition wraps silently past Integer.MAX_VALUE ===");
+ int max = Integer.MAX_VALUE;
+ int wrapped = max + 1;
+ System.out.println("Integer.MAX_VALUE = " + max);
+ System.out.println("Integer.MAX_VALUE + 1 = " + wrapped + " (wrapped to Integer.MIN_VALUE, no exception)");
+ System.out.println("Integer.MIN_VALUE = " + Integer.MIN_VALUE);
+
+ System.out.println();
+ System.out.println("=== A realistic case: summing item prices in cents overflows int ===");
+ int priceCents = 1_800_000_000; // $18,000,000.00, a plausible large invoice line in cents
+ int quantity = 2;
+ int totalCents = priceCents * quantity;
+ System.out.println("priceCents * quantity = " + priceCents + " * " + quantity + " = " + totalCents
+ + " (should be 3,600,000,000 - this is wrong, and no exception was thrown)");
+
+ System.out.println();
+ System.out.println("=== Multiplication overflows just as silently as addition ===");
+ int a = 100_000;
+ int b = 100_000;
+ int product = a * b; // mathematically 10,000,000,000 - doesn't fit in an int
+ System.out.println(a + " * " + b + " = " + product + " (true value is 10,000,000,000)");
+
+ System.out.println();
+ System.out.println("=== long has the same problem, just a much higher ceiling ===");
+ long longMax = Long.MAX_VALUE;
+ long longWrapped = longMax + 1;
+ System.out.println("Long.MAX_VALUE = " + longMax);
+ System.out.println("Long.MAX_VALUE + 1 = " + longWrapped + " (wrapped to Long.MIN_VALUE)");
+ }
+}
diff --git a/arithmetic/src/main/java/com/ankurm/arithmetic/WideningAndBigIntegerDemo.java b/arithmetic/src/main/java/com/ankurm/arithmetic/WideningAndBigIntegerDemo.java
new file mode 100644
index 0000000..4d5aa1d
--- /dev/null
+++ b/arithmetic/src/main/java/com/ankurm/arithmetic/WideningAndBigIntegerDemo.java
@@ -0,0 +1,35 @@
+package com.ankurm.arithmetic;
+
+import java.math.BigInteger;
+
+/**
+ * Two more ways to avoid the overflow in {@link OverflowWrapDemo}: widen to {@code long} before
+ * adding (cheap, but only pushes the ceiling higher - it can still overflow), or switch to
+ * {@link BigInteger} for genuinely unbounded precision (more expensive, never overflows).
+ */
+public class WideningAndBigIntegerDemo {
+
+ public static void main(String[] args) {
+ System.out.println("=== Widening to long fixes the int-range overflow from OverflowWrapDemo ===");
+ int a = Integer.MAX_VALUE;
+ int bad = a + 1; // wraps, wrong
+ long good = (long) a + 1; // widened before the add, correct
+ System.out.println("int: Integer.MAX_VALUE + 1 = " + bad + " (wrong)");
+ System.out.println("long: (long) Integer.MAX_VALUE + 1 = " + good + " (correct)");
+
+ System.out.println();
+ System.out.println("=== Widening only raises the ceiling - long still overflows eventually ===");
+ long lmax = Long.MAX_VALUE;
+ long stillWraps = lmax + 1;
+ System.out.println("Long.MAX_VALUE + 1 = " + stillWraps + " (wrapped to Long.MIN_VALUE - same bug, bigger numbers)");
+
+ System.out.println();
+ System.out.println("=== BigInteger never overflows - it grows to fit the true value ===");
+ BigInteger big = BigInteger.valueOf(Long.MAX_VALUE).add(BigInteger.ONE);
+ System.out.println("BigInteger.valueOf(Long.MAX_VALUE).add(ONE) = " + big);
+
+ BigInteger huge = BigInteger.valueOf(2).pow(100);
+ System.out.println("BigInteger.valueOf(2).pow(100) = " + huge);
+ System.out.println("(that value has no exact long or int representation at all)");
+ }
+}
diff --git a/arithmetic/src/test/java/com/ankurm/arithmetic/ArithmeticClaimsTest.java b/arithmetic/src/test/java/com/ankurm/arithmetic/ArithmeticClaimsTest.java
new file mode 100644
index 0000000..610e58c
--- /dev/null
+++ b/arithmetic/src/test/java/com/ankurm/arithmetic/ArithmeticClaimsTest.java
@@ -0,0 +1,106 @@
+package com.ankurm.arithmetic;
+
+import org.junit.jupiter.api.RepeatedTest;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigInteger;
+import java.util.Random;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class ArithmeticClaimsTest {
+
+ @Test
+ void plainIntAdditionWrapsPastMaxValueInsteadOfThrowing() {
+ int wrapped = Integer.MAX_VALUE + 1;
+ assertEquals(Integer.MIN_VALUE, wrapped);
+ }
+
+ @Test
+ void addExactThrowsExactlyWhenTheTrueSumDoesNotFitAnInt() {
+ assertEquals(42, Math.addExact(20, 22));
+ assertThrows(ArithmeticException.class, () -> Math.addExact(Integer.MAX_VALUE, 1));
+ }
+
+ @Test
+ void multiplyExactThrowsExactlyWhenTheTrueProductDoesNotFitAnInt() {
+ assertEquals(42, Math.multiplyExact(6, 7));
+ assertThrows(ArithmeticException.class, () -> Math.multiplyExact(100_000, 100_000));
+ }
+
+ @Test
+ void absDoesNotThrowOnIntegerMinValueAndReturnsAWrongNegativeNumber() {
+ // Math.abs is NOT part of the exact family - this is the trap absExact exists to catch.
+ assertEquals(Integer.MIN_VALUE, Math.abs(Integer.MIN_VALUE));
+ }
+
+ @Test
+ void absExactThrowsOnIntegerMinValueInsteadOfReturningAWrongAnswer() {
+ assertThrows(ArithmeticException.class, () -> Math.absExact(Integer.MIN_VALUE));
+ }
+
+ @Test
+ void toIntExactThrowsWhenALongDoesNotFitInAnInt() {
+ assertEquals(2_000_000_000, Math.toIntExact(2_000_000_000L));
+ assertThrows(ArithmeticException.class, () -> Math.toIntExact(3_000_000_000L));
+ }
+
+ @Test
+ void wideningToLongBeforeAddingAvoidsTheIntRangeOverflow() {
+ int a = Integer.MAX_VALUE;
+ long widened = (long) a + 1;
+ assertEquals(2_147_483_648L, widened);
+ }
+
+ @Test
+ void bigIntegerNeverOverflowsRegardlessOfMagnitude() {
+ BigInteger huge = BigInteger.valueOf(Long.MAX_VALUE).add(BigInteger.ONE);
+ assertEquals(new BigInteger("9223372036854775808"), huge);
+ }
+
+ @RepeatedTest(5)
+ void bitTrickOverflowDetectorAgreesWithBigIntegerGroundTruthOnRandomPairs() {
+ Random random = new Random();
+ for (int i = 0; i < 100_000; i++) {
+ int x = random.nextInt();
+ int y = random.nextInt();
+
+ boolean detected = OverflowBitTrickDemo.additionOverflows(x, y);
+
+ BigInteger trueSum = BigInteger.valueOf(x).add(BigInteger.valueOf(y));
+ boolean actuallyOverflows = trueSum.compareTo(BigInteger.valueOf(Integer.MIN_VALUE)) < 0
+ || trueSum.compareTo(BigInteger.valueOf(Integer.MAX_VALUE)) > 0;
+
+ assertEquals(actuallyOverflows, detected,
+ () -> x + " + " + y + ": bit trick said overflow=" + detected
+ + " but true sum is " + trueSum);
+ }
+ }
+
+ @Test
+ void bitTrickAgreesWithMathAddExactOnKnownEdgeCases() {
+ int[][] cases = {
+ {2_000_000_000, 2_000_000_000},
+ {2_000_000_000, -1_000_000_000},
+ {Integer.MAX_VALUE, 1},
+ {Integer.MIN_VALUE, -1},
+ {Integer.MIN_VALUE, Integer.MIN_VALUE},
+ {100, 200},
+ {0, 0}
+ };
+ for (int[] c : cases) {
+ boolean detected = OverflowBitTrickDemo.additionOverflows(c[0], c[1]);
+ boolean addExactThrew = true;
+ try {
+ Math.addExact(c[0], c[1]);
+ addExactThrew = false;
+ } catch (ArithmeticException ignored) {
+ // expected on overflow
+ }
+ assertTrue(detected == addExactThrew,
+ c[0] + " + " + c[1] + ": bit trick=" + detected + " vs addExact threw=" + addExactThrew);
+ }
+ }
+}
diff --git a/pom.xml b/pom.xml
index 98c327c..af6da0c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -20,6 +20,7 @@