diff --git a/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava.sln b/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava.sln
new file mode 100644
index 000000000..9a0fda14a
--- /dev/null
+++ b/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava.sln
@@ -0,0 +1,34 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.31903.59
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CSharpVsJava", "CSharpVsJava\CSharpVsJava.csproj", "{D563DC6E-1CA4-4A6B-9477-5F6C94F699AD}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
+ Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {D563DC6E-1CA4-4A6B-9477-5F6C94F699AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D563DC6E-1CA4-4A6B-9477-5F6C94F699AD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D563DC6E-1CA4-4A6B-9477-5F6C94F699AD}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {D563DC6E-1CA4-4A6B-9477-5F6C94F699AD}.Debug|x64.Build.0 = Debug|Any CPU
+ {D563DC6E-1CA4-4A6B-9477-5F6C94F699AD}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {D563DC6E-1CA4-4A6B-9477-5F6C94F699AD}.Debug|x86.Build.0 = Debug|Any CPU
+ {D563DC6E-1CA4-4A6B-9477-5F6C94F699AD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D563DC6E-1CA4-4A6B-9477-5F6C94F699AD}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D563DC6E-1CA4-4A6B-9477-5F6C94F699AD}.Release|x64.ActiveCfg = Release|Any CPU
+ {D563DC6E-1CA4-4A6B-9477-5F6C94F699AD}.Release|x64.Build.0 = Release|Any CPU
+ {D563DC6E-1CA4-4A6B-9477-5F6C94F699AD}.Release|x86.ActiveCfg = Release|Any CPU
+ {D563DC6E-1CA4-4A6B-9477-5F6C94F699AD}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+EndGlobal
diff --git a/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/CSharpVsJava.csproj b/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/CSharpVsJava.csproj
new file mode 100644
index 000000000..ed9781c22
--- /dev/null
+++ b/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/CSharpVsJava.csproj
@@ -0,0 +1,10 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+
+
+
diff --git a/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/ConcurrencyDemo.cs b/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/ConcurrencyDemo.cs
new file mode 100644
index 000000000..7743a66ce
--- /dev/null
+++ b/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/ConcurrencyDemo.cs
@@ -0,0 +1,21 @@
+using System.Diagnostics;
+
+namespace CSharpVsJava;
+
+public static class ConcurrencyDemo
+{
+ public static async Task RunAsync()
+ {
+ var stopwatch = Stopwatch.StartNew();
+
+ var stock = await Task.WhenAll(Enumerable.Range(1, 10_000).Select(GetStockAsync));
+
+ Console.WriteLine($"{stock.Length:N0} stock checks in {stopwatch.ElapsedMilliseconds:N0} ms");
+ }
+
+ private static async Task GetStockAsync(int productId)
+ {
+ await Task.Delay(TimeSpan.FromSeconds(1)); // stands in for a database or HTTP call
+ return productId % 7;
+ }
+}
diff --git a/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/EverydayDemo.cs b/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/EverydayDemo.cs
new file mode 100644
index 000000000..650a49889
--- /dev/null
+++ b/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/EverydayDemo.cs
@@ -0,0 +1,23 @@
+namespace CSharpVsJava;
+
+public record Money(decimal Amount)
+{
+ public static Money operator +(Money left, Money right) => new(left.Amount + right.Amount);
+}
+
+public class Product
+{
+ public required string Name { get; init; }
+ public required Money Price { get; set; }
+}
+
+public static class EverydayDemo
+{
+ public static void Run()
+ {
+ var lamp = new Product { Name = "Desk lamp", Price = new Money(30m) };
+ lamp.Price += new Money(5m);
+
+ Console.WriteLine($"{lamp.Name} costs {lamp.Price.Amount}");
+ }
+}
diff --git a/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/GenericsDemo.cs b/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/GenericsDemo.cs
new file mode 100644
index 000000000..5148bc197
--- /dev/null
+++ b/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/GenericsDemo.cs
@@ -0,0 +1,16 @@
+namespace CSharpVsJava;
+
+public static class GenericsDemo
+{
+ public static void Run()
+ {
+ var quantities = new List { 3, 1, 2 };
+ var customers = new List { "Ann", "Bob" };
+
+ Console.WriteLine(quantities.GetType());
+ Console.WriteLine(quantities.GetType() == customers.GetType());
+ Console.WriteLine(Describe(quantities));
+ }
+
+ private static string Describe(List items) => $"{items.Count} items of type {typeof(T).Name}";
+}
diff --git a/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/LinqDemo.cs b/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/LinqDemo.cs
new file mode 100644
index 000000000..eafb1612d
--- /dev/null
+++ b/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/LinqDemo.cs
@@ -0,0 +1,24 @@
+namespace CSharpVsJava;
+
+public record Order(string Customer, int Quantity);
+
+public static class LinqDemo
+{
+ public static void Run()
+ {
+ var orders = new List
+ {
+ new("Ann", 3), new("Bob", 1), new("Ann", 4), new("Cid", 5), new("Bob", 2)
+ };
+
+ var totals = orders
+ .GroupBy(order => order.Customer)
+ .Select(group => new { Customer = group.Key, Items = group.Sum(order => order.Quantity) })
+ .OrderByDescending(total => total.Items);
+
+ foreach (var total in totals)
+ {
+ Console.WriteLine($"{total.Customer}: {total.Items}");
+ }
+ }
+}
diff --git a/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/MemoryDemo.cs b/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/MemoryDemo.cs
new file mode 100644
index 000000000..cefd63bc9
--- /dev/null
+++ b/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/MemoryDemo.cs
@@ -0,0 +1,19 @@
+namespace CSharpVsJava;
+
+public static class MemoryDemo
+{
+ public static void Run()
+ {
+ const int count = 1_000_000;
+ var before = GC.GetAllocatedBytesForCurrentThread();
+
+ var quantities = new List(count);
+ for (var i = 0; i < count; i++)
+ {
+ quantities.Add(i);
+ }
+
+ var allocated = GC.GetAllocatedBytesForCurrentThread() - before;
+ Console.WriteLine($"List with {quantities.Count:N0} items: {allocated / 1_000_000.0:F1} MB allocated");
+ }
+}
diff --git a/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/NullDemo.cs b/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/NullDemo.cs
new file mode 100644
index 000000000..dc38beed3
--- /dev/null
+++ b/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/NullDemo.cs
@@ -0,0 +1,18 @@
+namespace CSharpVsJava;
+
+public static class NullDemo
+{
+ public static void RunUnchecked()
+ {
+ string? email = FindEmail(customerId: 42);
+ Console.WriteLine(email.Length);
+ }
+
+ public static void Run()
+ {
+ string? email = FindEmail(customerId: 42);
+ Console.WriteLine(email?.Length ?? 0);
+ }
+
+ private static string? FindEmail(int customerId) => customerId == 1 ? "ann@example.com" : null;
+}
diff --git a/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/Program.cs b/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/Program.cs
new file mode 100644
index 000000000..cb112236a
--- /dev/null
+++ b/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/Program.cs
@@ -0,0 +1,16 @@
+using CSharpVsJava;
+
+switch (args.FirstOrDefault())
+{
+ case "generics": GenericsDemo.Run(); break;
+ case "memory": MemoryDemo.Run(); break;
+ case "concurrency": await ConcurrencyDemo.RunAsync(); break;
+ case "linq": LinqDemo.Run(); break;
+ case "shapes": ShapesDemo.Run(); break;
+ case "nulls": NullDemo.Run(); break;
+ case "nulls-unchecked": NullDemo.RunUnchecked(); break;
+ case "everyday": EverydayDemo.Run(); break;
+ default:
+ Console.WriteLine("Pass one of: generics, memory, concurrency, linq, shapes, nulls, nulls-unchecked, everyday");
+ break;
+}
diff --git a/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/ShapesDemo.cs b/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/ShapesDemo.cs
new file mode 100644
index 000000000..867b2853a
--- /dev/null
+++ b/csharp-intermediate-topics/CSharpVsJava/CSharpVsJava/ShapesDemo.cs
@@ -0,0 +1,27 @@
+namespace CSharpVsJava;
+
+public abstract record Shape;
+public record Circle(double Radius) : Shape;
+public record Square(double Side) : Shape;
+
+public static class ShapesDemo
+{
+ public static void Run()
+ {
+ Shape[] shapes = [new Circle(1), new Square(2)];
+ foreach (var shape in shapes)
+ {
+ Console.WriteLine($"{shape}: {Area(shape):F2}");
+ }
+
+ var bigger = new Circle(1) with { Radius = 3 };
+ Console.WriteLine(bigger);
+ }
+
+ private static double Area(Shape shape) => shape switch
+ {
+ Circle(var radius) => Math.PI * radius * radius,
+ Square(var side) => side * side,
+ _ => throw new ArgumentOutOfRangeException(nameof(shape))
+ };
+}
diff --git a/csharp-intermediate-topics/CSharpVsJava/README.md b/csharp-intermediate-topics/CSharpVsJava/README.md
new file mode 100644
index 000000000..cad0f4f31
--- /dev/null
+++ b/csharp-intermediate-topics/CSharpVsJava/README.md
@@ -0,0 +1,42 @@
+# C# vs Java: Which One Should You Pick?
+
+Sample code for the Code Maze article [C# vs Java: Which One Should You Pick?](https://code-maze.com/csharp-vs-java/).
+
+Each difference the article shows is one small program in both languages:
+
+| Demo | C# (console app) | Java (single-file program) |
+|---|---|---|
+| Generics at runtime | `GenericsDemo.cs` | `java/Generics.java` |
+| Memory for a million numbers | `MemoryDemo.cs` | `java/Memory.java` |
+| 10,000 one-second waits | `ConcurrencyDemo.cs` | `java/Concurrency.java` |
+| Grouping query | `LinqDemo.cs` | `java/Orders.java` |
+| Records and pattern matching | `ShapesDemo.cs` | `java/Shapes.java` |
+| Null safety | `NullDemo.cs` | `java/Nulls.java` |
+| Properties, operators, checked exceptions | `EverydayDemo.cs` | `java/Everyday.java` |
+
+## Running the C# demos
+
+You need the .NET 10 SDK. From this folder, pass the name of one demo:
+
+```
+dotnet run --project CSharpVsJava -- generics
+```
+
+The names are `generics`, `memory`, `concurrency`, `linq`, `shapes`, `nulls`, `nulls-unchecked` and `everyday`.
+
+The build prints one warning on purpose, CS8602 in `NullDemo.RunUnchecked()`, and `nulls-unchecked` ends in a `NullReferenceException`. That is the bug the article asks the compiler to catch.
+
+## Running the Java programs
+
+You need JDK 25 or later. Each file runs on its own, with no Maven or Gradle project:
+
+```
+cd java
+java Generics.java
+```
+
+`Nulls.java` ends in a `NullPointerException` (exit code 1) on purpose, and `Everyday.java` expects no `orders.csv` in the folder it runs from.
+
+## A note on CI
+
+The repository's CI builds this folder through `CSharpVsJava.sln`, so it compiles the C# project only. It does not compile or run the files in `java/`. They are here so the reader can run both sides, and we ran them with Eclipse Temurin 25 when we wrote the article.
diff --git a/csharp-intermediate-topics/CSharpVsJava/java/Concurrency.java b/csharp-intermediate-topics/CSharpVsJava/java/Concurrency.java
new file mode 100644
index 000000000..17351f3be
--- /dev/null
+++ b/csharp-intermediate-topics/CSharpVsJava/java/Concurrency.java
@@ -0,0 +1,20 @@
+void main() throws Exception {
+ long start = System.nanoTime();
+
+ try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
+ var checks = IntStream.rangeClosed(1, 10_000)
+ .mapToObj(id -> executor.submit(() -> getStock(id)))
+ .toList();
+ for (var check : checks) {
+ check.get();
+ }
+ }
+
+ long elapsed = Duration.ofNanos(System.nanoTime() - start).toMillis();
+ IO.println("%,d stock checks in %,d ms".formatted(10_000, elapsed));
+}
+
+int getStock(int productId) throws InterruptedException {
+ Thread.sleep(Duration.ofSeconds(1)); // stands in for a database or HTTP call
+ return productId % 7;
+}
diff --git a/csharp-intermediate-topics/CSharpVsJava/java/Everyday.java b/csharp-intermediate-topics/CSharpVsJava/java/Everyday.java
new file mode 100644
index 000000000..143fb3e7c
--- /dev/null
+++ b/csharp-intermediate-topics/CSharpVsJava/java/Everyday.java
@@ -0,0 +1,31 @@
+record Money(BigDecimal amount) {
+ Money add(Money other) {
+ return new Money(amount.add(other.amount));
+ }
+}
+
+class Product {
+ private final String name;
+ private Money price;
+
+ Product(String name, Money price) {
+ this.name = name;
+ this.price = price;
+ }
+
+ String getName() { return name; }
+ Money getPrice() { return price; }
+ void setPrice(Money price) { this.price = price; }
+}
+
+void main() {
+ var lamp = new Product("Desk lamp", new Money(new BigDecimal("30")));
+ lamp.setPrice(lamp.getPrice().add(new Money(new BigDecimal("5"))));
+ IO.println(lamp.getName() + " costs " + lamp.getPrice().amount());
+
+ try {
+ IO.println(Files.readString(Path.of("orders.csv")));
+ } catch (IOException e) {
+ IO.println("Could not read " + e.getMessage());
+ }
+}
diff --git a/csharp-intermediate-topics/CSharpVsJava/java/Generics.java b/csharp-intermediate-topics/CSharpVsJava/java/Generics.java
new file mode 100644
index 000000000..5943bf80f
--- /dev/null
+++ b/csharp-intermediate-topics/CSharpVsJava/java/Generics.java
@@ -0,0 +1,12 @@
+void main() {
+ var quantities = new ArrayList(List.of(3, 1, 2));
+ var customers = new ArrayList(List.of("Ann", "Bob"));
+
+ IO.println(quantities.getClass());
+ IO.println(quantities.getClass() == customers.getClass());
+ IO.println(describe(quantities, Integer.class));
+}
+
+ String describe(List items, Class type) {
+ return items.size() + " items of type " + type.getSimpleName();
+}
diff --git a/csharp-intermediate-topics/CSharpVsJava/java/Memory.java b/csharp-intermediate-topics/CSharpVsJava/java/Memory.java
new file mode 100644
index 000000000..66dfef3c6
--- /dev/null
+++ b/csharp-intermediate-topics/CSharpVsJava/java/Memory.java
@@ -0,0 +1,17 @@
+import com.sun.management.ThreadMXBean;
+import java.lang.management.ManagementFactory;
+
+void main() {
+ final int count = 1_000_000;
+ var threads = (ThreadMXBean) ManagementFactory.getThreadMXBean();
+ long before = threads.getCurrentThreadAllocatedBytes();
+
+ var quantities = new ArrayList(count);
+ for (int i = 0; i < count; i++) {
+ quantities.add(i);
+ }
+
+ long allocated = threads.getCurrentThreadAllocatedBytes() - before;
+ IO.println("ArrayList with %,d items: %.1f MB allocated"
+ .formatted(quantities.size(), allocated / 1_000_000.0));
+}
diff --git a/csharp-intermediate-topics/CSharpVsJava/java/Nulls.java b/csharp-intermediate-topics/CSharpVsJava/java/Nulls.java
new file mode 100644
index 000000000..13ebc9fd9
--- /dev/null
+++ b/csharp-intermediate-topics/CSharpVsJava/java/Nulls.java
@@ -0,0 +1,14 @@
+void main() {
+ IO.println(findEmail(42).map(String::length).orElse(0));
+
+ String email = findEmailOrNull(42);
+ IO.println(email.length());
+}
+
+Optional findEmail(int customerId) {
+ return customerId == 1 ? Optional.of("ann@example.com") : Optional.empty();
+}
+
+String findEmailOrNull(int customerId) {
+ return customerId == 1 ? "ann@example.com" : null;
+}
diff --git a/csharp-intermediate-topics/CSharpVsJava/java/Orders.java b/csharp-intermediate-topics/CSharpVsJava/java/Orders.java
new file mode 100644
index 000000000..195b3efe6
--- /dev/null
+++ b/csharp-intermediate-topics/CSharpVsJava/java/Orders.java
@@ -0,0 +1,13 @@
+record Order(String customer, int quantity) {}
+
+void main() {
+ var orders = List.of(
+ new Order("Ann", 3), new Order("Bob", 1), new Order("Ann", 4),
+ new Order("Cid", 5), new Order("Bob", 2));
+
+ orders.stream()
+ .collect(Collectors.groupingBy(Order::customer, Collectors.summingInt(Order::quantity)))
+ .entrySet().stream()
+ .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
+ .forEach(total -> IO.println(total.getKey() + ": " + total.getValue()));
+}
diff --git a/csharp-intermediate-topics/CSharpVsJava/java/Shapes.java b/csharp-intermediate-topics/CSharpVsJava/java/Shapes.java
new file mode 100644
index 000000000..4c5008a93
--- /dev/null
+++ b/csharp-intermediate-topics/CSharpVsJava/java/Shapes.java
@@ -0,0 +1,17 @@
+sealed interface Shape permits Circle, Square {}
+record Circle(double radius) implements Shape {}
+record Square(double side) implements Shape {}
+
+void main() {
+ List shapes = List.of(new Circle(1), new Square(2));
+ for (var shape : shapes) {
+ IO.println("%s: %.2f".formatted(shape, area(shape)));
+ }
+}
+
+double area(Shape shape) {
+ return switch (shape) {
+ case Circle(double radius) -> Math.PI * radius * radius;
+ case Square(double side) -> side * side;
+ };
+}