Table of Contents
Open Table of Contents
- The Short Version
- Comparison Matrix: What Changes in Production?
- A Fair Performance Comparison
- Learning Paths: From Syntax to Production
- The Shared Scenario and How to Run It
- 1. Validate an Order Line at the Boundary
- 2. Calculate Money Without Floating-Point Surprises
- 3. Reject Invalid Order-State Transitions
- 4. Aggregate Paid Revenue by Customer
- 5. Import Inventory With Line-Numbered Errors
- 6. Deduplicate Webhook IDs While Preserving Order
- 7. Process Files Concurrently Without Unlimited Fan-Out
- 8. Reserve Stock Without a Check-Then-Act Race
- 9. Retry Only an Explicitly Transient Failure
- 10. Publish a Report Without Exposing a Partial File
- Correctness Checklist: Make the Differences Observable
- Which Language Would I Choose?
- Official References
The Short Version
Java and Python are industry veterans, not obsolete tools. Go and Rust are younger alternatives, not automatic upgrades. Java optimizes for large application ecosystems, Python for development speed and data workflows, Go for straightforward concurrent services, and Rust for control with strong compile-time guarantees.
The useful question is not “Which language wins?” It is “Which costs can this team afford: runtime resources, implementation time, operational complexity, or correctness work?”
Below: a comparison matrix, learning paths, and 10 order-processing scenarios with four implementations each: 40 complete programs. The inputs and business rules stay comparable so the differences in code, error handling, and concurrency remain visible.
Version check: Python 3.15, not Python 15
As of November 2, 2026, OpenJDK lists JDK 27 as generally available since September 15, 2026. Python’s release schedule records Python 3.15.0 on October 9, 2026. “Python 15” here means Python 3.15.
Java 27 is a feature release; do not assume the same maintenance commitment as a vendor-supported JDK 25 LTS distribution. Select a maintained patch release and verify the distributor’s support policy. Likewise, use a maintained Python 3.15 patch release, not necessarily 3.15.0.
This is a language-and-runtime comparison, not a list of new JDK 27 or Python 3.15 features. The examples deliberately use established, non-preview APIs. Go and Rust examples use stable language features rather than a particular release’s novelty.
Comparison Matrix: What Changes in Production?
These are engineering tendencies, not benchmark results or universal rankings.
| Dimension | Java 27 | Python 3.15 / CPython | Go | Rust |
|---|---|---|---|---|
| Execution model | JVM bytecode, adaptive JIT compilation, garbage collection | Dynamic runtime; interpreter/JIT behavior depends on build and configuration | Ahead-of-time native compilation, runtime scheduler, garbage collection | Ahead-of-time native compilation, ownership, no tracing GC |
| CPU-heavy application code | Often strong sustained throughput after warmup | Pure Python loops usually cost more; native libraries can dominate execution instead | Strong native performance with relatively simple code | Strong native performance and fine control over layout, allocation, and dispatch |
| Startup and warmup | JVM startup and JIT warmup matter; AOT techniques have separate tradeoffs | Interpreter startup plus imports; dependency-heavy applications can start slowly | Usually quick startup; initialization still matters | Usually quick startup; initialization and dynamic linking still matter |
| Memory and latency | Heap, allocation rate, collector, and warmup affect footprint and tail latency | Object overhead, reference counting, cyclic GC, and extension allocations matter | GC and allocation rate affect footprint and tail latency | No tracing-GC pauses; allocations, reference counting, locks, and OS scheduling still affect latency |
| Memory safety | Managed code prevents many memory errors; JNI/native code changes the boundary | Managed objects are memory-safe at language level; native extensions can violate that boundary | Safe Go provides memory safety; unsafe and cgo need separate scrutiny | Safe Rust enforces ownership and borrowing; unsafe, FFI, and unsound dependencies need auditing |
| Application security | Strong libraries and tooling; injection, authorization, and dependency bugs remain possible | Strong libraries; dynamic input handling and dependencies require discipline | Small standard tooling surface; authorization and input-validation bugs remain possible | Memory safety is not authorization, cryptographic correctness, or protection from business-logic bugs |
| Concurrency | Threads, virtual threads, executors, locks; virtual threads help blocking I/O scale, not CPU throughput | asyncio, threads, processes; GIL-enabled and free-threaded builds have different constraints | Goroutines, channels, mutexes, and explicit cancellation through contexts | Threads, channels, locks; async generally uses an ecosystem runtime |
| Race prevention | Synchronization remains the programmer’s responsibility | Do not rely on the GIL to make multi-step business operations atomic | Synchronization remains essential; the race detector finds exercised races | Safe Rust rejects data races; logical races and deadlocks remain possible |
| Architectural sweet spot | Large transactional systems, modular monoliths, enterprise integration | Data/AI pipelines, automation, APIs, rapid product iteration | Network services, infrastructure tools, agents, CLIs | Systems components, parsers, performance-sensitive services, embedded software |
| API and data ecosystem | Spring, Jakarta, Jackson, JDBC, mature observability integrations | Django, FastAPI, scientific/ML libraries, extensive data tooling | Strong standard HTTP tooling, database interfaces, compact service stacks | Axum, Tokio, Serde, SQLx and others; explicit choices and ownership constraints |
| Type and error model | Static types, exceptions, records, interfaces, generics | Dynamic types, exceptions, optional static checking | Static types, interfaces, explicit error returns | Static types, traits, algebraic data types, Result and Option |
| Packaging and delivery | JAR plus runtime or a custom runtime image; native-image builds are a separate option | Interpreter, environment, dependencies, and potentially platform-specific wheels | Often one executable; cgo, certificates, and assets can add dependencies | Native executable; libc, native libraries, target platform, and assets still matter |
| Build and feedback cost | Mature build tools; large builds and annotation processing can be expensive | Fast edit/run loop; environment resolution and native packages can complicate setup | Integrated tooling and typically fast builds | Thorough compiler feedback; generics and optimization can increase build time |
| Learning curve | Moderate syntax; enterprise frameworks and JVM operations add depth | Accessible syntax; packaging, typing, concurrency, and production design still take work | Small language surface; cancellation, interfaces, and concurrency design are the hard parts | Ownership, lifetimes, traits, and async create a steeper initial climb |
| Team and maintenance cost | Large existing estates and experienced teams; framework discipline matters | Broad adoption across roles; tests and type checking help larger teams | Consistent tooling helps shared maintenance; explicit error paths add code | Compiler guarantees reduce some defect classes; onboarding and specialist knowledge cost more |
Architecture is not dictated by language. All four can implement a modular monolith, a service, or a worker. Splitting a system into microservices adds network failures, distributed transactions, and operational work regardless of syntax.
For concurrency details, consult Java virtual threads, Python free-threading guidance, the Go memory model, and Rust’s ownership model. Free-threaded Python requires compatible builds and extensions; neither “Python can never use multiple cores” nor “all Python code now runs in parallel” is a safe blanket claim.
A Fair Performance Comparison
Do not compare a Python loop with a Rust implementation that uses a different algorithm, or a cold JVM with a warmed native service and call the result a language ranking.
For an order-processing service, keep the dataset, algorithm, database, connection limits, request mix, resource limits, and hardware equivalent. Record runtime versions, compiler flags, dependencies, and Python’s GIL/JIT configuration.
Measure startup separately from steady state. Then measure throughput, p50/p95/p99 latency, resident memory, CPU time, allocation/GC behavior, and error rate at the same offered load. Include slow dependencies, saturation, and recovery. Report repeated runs and variation, not just the best number.
Use optimized Rust builds, a representative Go build, warmed Java where appropriate, and the actual Python stack you plan to ship. Native numerical libraries can make Python an excellent orchestrator of fast code; that does not make arbitrary Python loops equally fast. Virtual threads and goroutines improve concurrency ergonomics, not the speed of a CPU-bound algorithm.
The programs below demonstrate implementation complexity and correctness boundaries, not performance measurements.
Learning Paths: From Syntax to Production
| Language | Start here | Build next | Production skills |
|---|---|---|---|
| Java | Types, collections, records, exceptions, interfaces | A small service with tests, JDBC, and explicit transactions | JVM profiling, heap/GC analysis, dependency upgrades, concurrency limits, framework lifecycle |
| Python | Functions, collections, exceptions, context managers, type hints | A typed API or worker with isolated environments and tests | Packaging, static checking, profiling, async/process choices, native-extension compatibility |
| Go | Structs, slices, maps, interfaces, error handling | A CLI and HTTP service using the standard library | Context deadlines, goroutine lifecycle, race tests, profiling, resource limits |
| Rust | Ownership, borrowing, enums, Result, iterators | A synchronous CLI with tests before introducing async | Traits/lifetimes, profiling, async cancellation, FFI boundaries, dependency and unsafe-code review |
Useful starting points: dev.java Learn, the Python tutorial, A Tour of Go, and The Rust Book. Progress by building and explaining failure cases, not by promising mastery in a fixed number of days.
The Shared Scenario and How to Run It
Imagine a small commerce backend. It validates order lines, computes money, moves orders through states, imports inventory, produces reports, deduplicates events, and protects stock.
Each code block is an independent, complete program. Save one example at a time; do not concatenate all ten programs. They use only standard libraries, intentionally keeping framework setup out of the comparison. That constraint is not a recommendation to hand-write production JSON, SQL drivers, or HTTP frameworks.
Use JDK 27, Python 3.15, and maintained stable Go/Rust toolchains. The Rust examples use edition 2024. The Python examples also use syntax and APIs available in Python 3.12; they do not depend on 3.15-only features.
| Language | Save as | Run |
|---|---|---|
| Java | Main.java | java Main.java |
| Python | main.py | python3 main.py |
| Go | main.go | go run main.go |
| Rust | main.rs | rustc --edition=2024 main.rs -o demo && ./demo |
Check java --version, python3 --version, go version, and rustc --version first. The command name python3 alone does not guarantee Python 3.15.
For scenarios 5 and 7, create these small fixtures in your working directory:
printf 'BOOK-1\t2\nPEN-2\t5\n' > inventory.tsv
printf 'INFO\tstart\nERROR\tpayment\nERROR\tstock\n' > audit-1.log
printf 'ERROR\ttimeout\n' > audit-2.log
printf 'INFO\tcomplete\n' > audit-3.log
printf 'ERROR\tshipping\n' > audit-4.log
Fixtures are controlled UTF-8/ASCII files. Scenario 7 assumes producer-bounded log lines, not arbitrarily large hostile records. Scenario 10 writes report.csv in the current directory and targets a local Linux filesystem.
1. Validate an Order Line at the Boundary
Contract: SKU contains 1-24 uppercase ASCII letters, digits, or hyphens. Quantity text contains one or two ASCII digits and represents 1-10 units. Leading zeroes are allowed. Valid input prints BOOK-1:2; malformed input must fail, not silently become zero.
Java
public class Main {
record OrderLine(String sku, int quantity) {}
static OrderLine parse(String sku, String rawQuantity) {
if (!sku.matches("[A-Z0-9-]{1,24}") ||
!rawQuantity.matches("[0-9]{1,2}")) {
throw new IllegalArgumentException("Invalid order line");
}
int quantity = Integer.parseInt(rawQuantity);
if (quantity < 1 || quantity > 10) {
throw new IllegalArgumentException("Quantity must be 1..10");
}
return new OrderLine(sku, quantity);
}
public static void main(String[] args) {
var line = parse("BOOK-1", "2");
System.out.println(line.sku() + ":" + line.quantity());
}
}
Python
from dataclasses import dataclass
import re
@dataclass(frozen=True)
class OrderLine:
sku: str
quantity: int
def parse(sku: str, raw_quantity: str) -> OrderLine:
if not re.fullmatch(r"[A-Z0-9-]{1,24}", sku):
raise ValueError("Invalid SKU")
if not re.fullmatch(r"[0-9]{1,2}", raw_quantity):
raise ValueError("Invalid quantity")
quantity = int(raw_quantity)
if not 1 <= quantity <= 10:
raise ValueError("Quantity must be 1..10")
return OrderLine(sku, quantity)
line = parse("BOOK-1", "2")
print(f"{line.sku}:{line.quantity}")
Go
package main
import (
"fmt"
"log"
"regexp"
"strconv"
)
type OrderLine struct {
SKU string
Quantity int
}
var skuPattern = regexp.MustCompile(`^[A-Z0-9-]{1,24}$`)
var quantityPattern = regexp.MustCompile(`^[0-9]{1,2}$`)
func parse(sku, rawQuantity string) (OrderLine, error) {
if !skuPattern.MatchString(sku) || !quantityPattern.MatchString(rawQuantity) {
return OrderLine{}, fmt.Errorf("invalid order line")
}
quantity, err := strconv.Atoi(rawQuantity)
if err != nil {
return OrderLine{}, err
}
if quantity < 1 || quantity > 10 {
return OrderLine{}, fmt.Errorf("quantity must be 1..10")
}
return OrderLine{sku, quantity}, nil
}
func main() {
line, err := parse("BOOK-1", "2")
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s:%d\n", line.SKU, line.Quantity)
}
Rust
struct OrderLine {
sku: String,
quantity: u32,
}
fn parse(sku: &str, raw_quantity: &str) -> Result<OrderLine, &'static str> {
if !(1..=24).contains(&sku.len())
|| !sku.bytes().all(|b| b.is_ascii_uppercase() || b.is_ascii_digit() || b == b'-')
|| !(1..=2).contains(&raw_quantity.len())
|| !raw_quantity.bytes().all(|b| b.is_ascii_digit())
{
return Err("invalid order line");
}
let quantity: u32 = raw_quantity.parse().map_err(|_| "invalid quantity")?;
if !(1..=10).contains(&quantity) {
return Err("quantity must be 1..10");
}
Ok(OrderLine { sku: sku.to_owned(), quantity })
}
fn main() -> Result<(), &'static str> {
let line = parse("BOOK-1", "2")?;
println!("{}:{}", line.sku, line.quantity);
Ok(())
}
Complexity signal: Python expresses the data shape with little ceremony; Java records remove much historical boilerplate. Go makes the error branch visible at the call site. Rust exposes both fallibility and the conversion from borrowed input into owned data. These are string-input functions; an HTTP adapter must still reject missing/null fields and incorrect JSON types.
2. Calculate Money Without Floating-Point Surprises
Contract: nonnegative unit price up to 1,000,000,000 cents, quantity 1-10,000, and tax 0-10,000 basis points. Calculate tax on the subtotal, rounding half up once. The bounds keep intermediates within signed 64-bit range.
For 1,999 cents, three units, and 825 basis points: subtotal = 5,997; tax = 495; total = 6,492 cents. This assumes a two-decimal currency and a deliberately specified tax policy, not every jurisdiction’s rules.
Java
public class Main {
static long total(long unitCents, long quantity, long taxBps) {
if (unitCents < 0 || unitCents > 1_000_000_000L ||
quantity < 1 || quantity > 10_000 ||
taxBps < 0 || taxBps > 10_000) {
throw new IllegalArgumentException("Pricing input out of range");
}
long subtotal = unitCents * quantity;
long tax = (subtotal * taxBps + 5_000) / 10_000;
return subtotal + tax;
}
public static void main(String[] args) {
System.out.println(total(1_999, 3, 825));
}
}
Python
def total(unit_cents: int, quantity: int, tax_bps: int) -> int:
if not (
0 <= unit_cents <= 1_000_000_000
and 1 <= quantity <= 10_000
and 0 <= tax_bps <= 10_000
):
raise ValueError("Pricing input out of range")
subtotal = unit_cents * quantity
tax = (subtotal * tax_bps + 5_000) // 10_000
return subtotal + tax
print(total(1_999, 3, 825))
Go
package main
import (
"fmt"
"log"
)
func total(unitCents, quantity, taxBps int64) (int64, error) {
if unitCents < 0 || unitCents > 1_000_000_000 ||
quantity < 1 || quantity > 10_000 ||
taxBps < 0 || taxBps > 10_000 {
return 0, fmt.Errorf("pricing input out of range")
}
subtotal := unitCents * quantity
tax := (subtotal*taxBps + 5_000) / 10_000
return subtotal + tax, nil
}
func main() {
cents, err := total(1_999, 3, 825)
if err != nil {
log.Fatal(err)
}
fmt.Println(cents)
}
Rust
fn total(unit_cents: i64, quantity: i64, tax_bps: i64) -> Result<i64, &'static str> {
if !(0..=1_000_000_000).contains(&unit_cents)
|| !(1..=10_000).contains(&quantity)
|| !(0..=10_000).contains(&tax_bps)
{
return Err("pricing input out of range");
}
let subtotal = unit_cents * quantity;
let tax = (subtotal * tax_bps + 5_000) / 10_000;
Ok(subtotal + tax)
}
fn main() -> Result<(), &'static str> {
println!("{}", total(1_999, 3, 825)?);
Ok(())
}
Complexity signal: the business rule is almost identical. Correct units, bounds, and rounding matter more than syntax. Python’s type hints are not runtime validation: callers must supply integers, not floats or booleans. If inputs come directly from JSON, validate their types before calling this internal function.
3. Reject Invalid Order-State Transitions
Contract: new orders can be paid or cancelled; paid orders can be shipped or cancelled. Shipped/cancelled orders have no outgoing transitions. Paying and then shipping prints state-ok; shipping a new order must fail.
Java
public class Main {
enum State { NEW, PAID, SHIPPED, CANCELLED }
enum Event { PAY, SHIP, CANCEL }
static State transition(State state, Event event) {
return switch (state) {
case NEW -> switch (event) {
case PAY -> State.PAID;
case CANCEL -> State.CANCELLED;
default -> throw new IllegalStateException("Invalid transition");
};
case PAID -> switch (event) {
case SHIP -> State.SHIPPED;
case CANCEL -> State.CANCELLED;
default -> throw new IllegalStateException("Invalid transition");
};
default -> throw new IllegalStateException("Terminal order");
};
}
public static void main(String[] args) {
var state = transition(transition(State.NEW, Event.PAY), Event.SHIP);
if (state != State.SHIPPED) throw new AssertionError(state);
System.out.println("state-ok");
}
}
Python
from enum import Enum, auto
class State(Enum):
NEW = auto()
PAID = auto()
SHIPPED = auto()
CANCELLED = auto()
class Event(Enum):
PAY = auto()
SHIP = auto()
CANCEL = auto()
def transition(state: State, event: Event) -> State:
match (state, event):
case (State.NEW, Event.PAY):
return State.PAID
case (State.PAID, Event.SHIP):
return State.SHIPPED
case (State.NEW | State.PAID, Event.CANCEL):
return State.CANCELLED
case _:
raise ValueError("Invalid transition")
state = transition(transition(State.NEW, Event.PAY), Event.SHIP)
assert state == State.SHIPPED
print("state-ok")
Go
package main
import (
"fmt"
"log"
)
type State string
type Event string
const (
New State = "new"
Paid State = "paid"
Shipped State = "shipped"
Cancelled State = "cancelled"
Pay Event = "pay"
Ship Event = "ship"
Cancel Event = "cancel"
)
func transition(state State, event Event) (State, error) {
switch {
case state == New && event == Pay:
return Paid, nil
case state == Paid && event == Ship:
return Shipped, nil
case (state == New || state == Paid) && event == Cancel:
return Cancelled, nil
default:
return "", fmt.Errorf("invalid transition: %s/%s", state, event)
}
}
func main() {
state, err := transition(New, Pay)
if err != nil {
log.Fatal(err)
}
state, err = transition(state, Ship)
if err != nil {
log.Fatal(err)
}
if state != Shipped {
log.Fatal("unexpected state")
}
fmt.Println("state-ok")
}
Rust
#[derive(Clone, Copy, PartialEq, Debug)]
enum State { New, Paid, Shipped, Cancelled }
enum Event { Pay, Ship, Cancel }
fn transition(state: State, event: Event) -> Result<State, &'static str> {
match (state, event) {
(State::New, Event::Pay) => Ok(State::Paid),
(State::Paid, Event::Ship) => Ok(State::Shipped),
(State::New | State::Paid, Event::Cancel) => Ok(State::Cancelled),
_ => Err("invalid transition"),
}
}
fn main() -> Result<(), &'static str> {
let state = transition(transition(State::New, Event::Pay)?, Event::Ship)?;
assert_eq!(state, State::Shipped);
assert_eq!(transition(State::New, Event::Cancel)?, State::Cancelled);
println!("state-ok");
Ok(())
}
Complexity signal: Rust naturally models a closed set of states and events. Java also has closed enums and switch expressions; Python’s pattern matching is expressive but does not enforce exhaustive handling at runtime. Go’s named string types still allow other strings, so validation matters. None of these functions makes two database updates atomic; persistent transitions need a transaction or version check.
4. Aggregate Paid Revenue by Customer
Contract: sum only paid rows, sort customers by key, and output alice=5000 followed by bob=2500. Rows are already validated; the batch total is guaranteed to fit signed 64-bit cents. This is aggregation, not a revenue-recognition policy.
Java
import java.util.List;
import java.util.TreeMap;
public class Main {
record Sale(String customer, long cents, boolean paid) {}
public static void main(String[] args) {
var sales = List.of(
new Sale("alice", 1999, true),
new Sale("bob", 2500, true),
new Sale("alice", 800, false),
new Sale("alice", 3001, true)
);
var totals = new TreeMap<String, Long>();
for (var sale : sales) {
if (sale.paid()) {
totals.merge(sale.customer(), sale.cents(), Math::addExact);
}
}
totals.forEach((customer, cents) ->
System.out.println(customer + "=" + cents));
}
}
Python
from collections import defaultdict
from dataclasses import dataclass
@dataclass(frozen=True)
class Sale:
customer: str
cents: int
paid: bool
sales = [
Sale("alice", 1999, True),
Sale("bob", 2500, True),
Sale("alice", 800, False),
Sale("alice", 3001, True),
]
totals: dict[str, int] = defaultdict(int)
for sale in sales:
if sale.paid:
totals[sale.customer] += sale.cents
for customer, cents in sorted(totals.items()):
print(f"{customer}={cents}")
Go
package main
import (
"fmt"
"sort"
)
type Sale struct {
Customer string
Cents int64
Paid bool
}
func main() {
sales := []Sale{
{"alice", 1999, true},
{"bob", 2500, true},
{"alice", 800, false},
{"alice", 3001, true},
}
totals := make(map[string]int64)
for _, sale := range sales {
if sale.Paid {
totals[sale.Customer] += sale.Cents
}
}
customers := make([]string, 0, len(totals))
for customer := range totals {
customers = append(customers, customer)
}
sort.Strings(customers)
for _, customer := range customers {
fmt.Printf("%s=%d\n", customer, totals[customer])
}
}
Rust
use std::collections::BTreeMap;
struct Sale<'a> {
customer: &'a str,
cents: i64,
paid: bool,
}
fn main() -> Result<(), &'static str> {
let sales = [
Sale { customer: "alice", cents: 1999, paid: true },
Sale { customer: "bob", cents: 2500, paid: true },
Sale { customer: "alice", cents: 800, paid: false },
Sale { customer: "alice", cents: 3001, paid: true },
];
let mut totals: BTreeMap<&str, i64> = BTreeMap::new();
for sale in &sales {
if sale.paid {
let total = totals.entry(sale.customer).or_default();
*total = total.checked_add(sale.cents).ok_or("revenue overflow")?;
}
}
for (customer, cents) in totals {
println!("{customer}={cents}");
}
Ok(())
}
Complexity signal: collection APIs shape the solution. Go needs explicit sorting because map iteration is unordered. Rust borrows customer names from the input instead of allocating new strings. For unbounded external batches, validate totals or use checked/arbitrary-precision arithmetic; the sample’s input bound is not a universal overflow guarantee.
5. Import Inventory With Line-Numbered Errors
Contract: read inventory.tsv, with exactly two tab-separated fields per line, no header or quoting, the same SKU rule as scenario 1, and quantity 1-10. Accept LF or CRLF line endings. Print BOOK-1:2 and PEN-2:5. Reject malformed rows with their line number.
This is a deliberately constrained TSV format, not a general CSV parser. Printing accepted rows demonstrates streaming; a production all-or-nothing inventory import must stage and validate before committing updates.
Java
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
public class Main {
public static void main(String[] args) throws Exception {
try (var reader = Files.newBufferedReader(
Path.of("inventory.tsv"), StandardCharsets.UTF_8)) {
String row;
int number = 0;
while ((row = reader.readLine()) != null) {
number++;
var fields = row.split("\t", -1);
if (fields.length != 2 ||
!fields[0].matches("[A-Z0-9-]{1,24}") ||
!fields[1].matches("[0-9]{1,2}")) {
throw new IllegalArgumentException("Invalid row " + number);
}
int quantity = Integer.parseInt(fields[1]);
if (quantity < 1 || quantity > 10) {
throw new IllegalArgumentException("Invalid row " + number);
}
System.out.println(fields[0] + ":" + quantity);
}
}
}
}
Python
import re
with open("inventory.tsv", encoding="utf-8", newline="") as source:
for number, row in enumerate(source, start=1):
fields = row.removesuffix("\n").removesuffix("\r").split("\t")
if (
len(fields) != 2
or not re.fullmatch(r"[A-Z0-9-]{1,24}", fields[0])
or not re.fullmatch(r"[0-9]{1,2}", fields[1])
):
raise ValueError(f"Invalid row {number}")
quantity = int(fields[1])
if not 1 <= quantity <= 10:
raise ValueError(f"Invalid row {number}")
print(f"{fields[0]}:{quantity}")
Go
package main
import (
"bufio"
"fmt"
"log"
"os"
"regexp"
"strconv"
"strings"
)
func run() error {
file, err := os.Open("inventory.tsv")
if err != nil {
return err
}
defer file.Close()
sku := regexp.MustCompile(`^[A-Z0-9-]{1,24}$`)
quantityText := regexp.MustCompile(`^[0-9]{1,2}$`)
scanner := bufio.NewScanner(file)
number := 0
for scanner.Scan() {
number++
fields := strings.Split(scanner.Text(), "\t")
if len(fields) != 2 || !sku.MatchString(fields[0]) ||
!quantityText.MatchString(fields[1]) {
return fmt.Errorf("invalid row %d", number)
}
quantity, err := strconv.Atoi(fields[1])
if err != nil || quantity < 1 || quantity > 10 {
return fmt.Errorf("invalid row %d", number)
}
fmt.Printf("%s:%d\n", fields[0], quantity)
}
if err := scanner.Err(); err != nil {
return fmt.Errorf("reading row %d: %w", number+1, err)
}
return nil
}
func main() {
if err := run(); err != nil {
log.Fatal(err)
}
}
Rust
use std::fs::File;
use std::io::{BufRead, BufReader};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let reader = BufReader::new(File::open("inventory.tsv")?);
for (index, row) in reader.lines().enumerate() {
let row = row?;
let fields: Vec<&str> = row.split('\t').collect();
let valid = fields.len() == 2
&& (1..=24).contains(&fields[0].len())
&& fields[0].bytes().all(|b|
b.is_ascii_uppercase() || b.is_ascii_digit() || b == b'-')
&& (1..=2).contains(&fields[1].len())
&& fields[1].bytes().all(|b| b.is_ascii_digit());
if !valid {
return Err(format!("invalid row {}", index + 1).into());
}
let quantity: u32 = fields[1].parse()?;
if !(1..=10).contains(&quantity) {
return Err(format!("invalid row {}", index + 1).into());
}
println!("{}:{quantity}", fields[0]);
}
Ok(())
}
Complexity signal: resource lifetime is implicit in Rust’s ownership, explicit through context managers in Python and try-with-resources in Java, and commonly managed with defer in Go. Go’s scanner also has a token-size limit; its error must be checked. For untrusted uploads, add upload/record limits before parsing in every language.
6. Deduplicate Webhook IDs While Preserving Order
Contract: for evt-2, evt-1, evt-2, evt-3, retain first occurrences and print evt-2,evt-1,evt-3. The input is one bounded batch of already-validated event IDs.
Java
import java.util.LinkedHashSet;
import java.util.List;
public class Main {
public static void main(String[] args) {
var events = List.of("evt-2", "evt-1", "evt-2", "evt-3");
var unique = new LinkedHashSet<>(events);
System.out.println(String.join(",", unique));
}
}
Python
events = ["evt-2", "evt-1", "evt-2", "evt-3"]
unique = list(dict.fromkeys(events))
print(",".join(unique))
Go
package main
import (
"fmt"
"strings"
)
func main() {
events := []string{"evt-2", "evt-1", "evt-2", "evt-3"}
seen := make(map[string]struct{})
unique := make([]string, 0, len(events))
for _, event := range events {
if _, exists := seen[event]; !exists {
seen[event] = struct{}{}
unique = append(unique, event)
}
}
fmt.Println(strings.Join(unique, ","))
}
Rust
use std::collections::HashSet;
fn main() {
let events = ["evt-2", "evt-1", "evt-2", "evt-3"];
let mut seen = HashSet::new();
let mut unique = Vec::new();
for event in events {
if seen.insert(event) {
unique.push(event);
}
}
println!("{}", unique.join(","));
}
Complexity signal: Python’s insertion-ordered dictionaries and Java’s linked sets make this short. Go and Rust express the ordered-output collection separately from membership checking. This is not durable idempotency: preventing duplicate charges across workers or restarts needs a persisted unique key and an atomic transaction tying the key to its effect. Authenticate webhooks before trusting their IDs.
7. Process Files Concurrently Without Unlimited Fan-Out
Contract: process four audit files with at most two active file readers, count lines beginning with ERROR followed by a tab, and print errors=4. Missing files and read errors must fail the operation.
Java, Python, and Go below use two workers. Rust statically partitions this four-file batch between two scoped threads. That meets the same concurrency bound but can balance uneven file sizes differently; these are not identical schedulers.
Java
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Main {
static long count(String path) throws Exception {
try (var lines = Files.lines(Path.of(path))) {
return lines.filter(line -> line.startsWith("ERROR\t")).count();
}
}
public static void main(String[] args) throws Exception {
var paths = List.of("audit-1.log", "audit-2.log", "audit-3.log", "audit-4.log");
try (var pool = Executors.newFixedThreadPool(2)) {
var results = new ArrayList<Future<Long>>();
for (var path : paths) {
results.add(pool.submit(() -> count(path)));
}
long total = 0;
for (var result : results) {
total += result.get();
}
System.out.println("errors=" + total);
}
}
}
Python
from concurrent.futures import ThreadPoolExecutor
def count(path: str) -> int:
with open(path, encoding="utf-8") as source:
return sum(line.startswith("ERROR\t") for line in source)
paths = ["audit-1.log", "audit-2.log", "audit-3.log", "audit-4.log"]
with ThreadPoolExecutor(max_workers=2) as pool:
total = sum(pool.map(count, paths))
print(f"errors={total}")
Go
package main
import (
"bufio"
"fmt"
"log"
"os"
"strings"
"sync"
)
type Result struct {
Count int
Err error
}
func count(path string) (int, error) {
file, err := os.Open(path)
if err != nil {
return 0, err
}
defer file.Close()
scanner := bufio.NewScanner(file)
total := 0
for scanner.Scan() {
if strings.HasPrefix(scanner.Text(), "ERROR\t") {
total++
}
}
return total, scanner.Err()
}
func run() error {
paths := []string{"audit-1.log", "audit-2.log", "audit-3.log", "audit-4.log"}
jobs := make(chan string, len(paths))
results := make(chan Result, len(paths))
var workers sync.WaitGroup
for i := 0; i < 2; i++ {
workers.Add(1)
go func() {
defer workers.Done()
for path := range jobs {
n, err := count(path)
results <- Result{n, err}
}
}()
}
for _, path := range paths {
jobs <- path
}
close(jobs)
workers.Wait()
close(results)
total := 0
for result := range results {
if result.Err != nil {
return result.Err
}
total += result.Count
}
fmt.Printf("errors=%d\n", total)
return nil
}
func main() {
if err := run(); err != nil {
log.Fatal(err)
}
}
Rust
use std::fs::File;
use std::io::{self, BufRead, BufReader};
use std::thread;
fn count(path: &str) -> io::Result<usize> {
let mut total = 0;
for line in BufReader::new(File::open(path)?).lines() {
if line?.starts_with("ERROR\t") {
total += 1;
}
}
Ok(total)
}
fn main() -> io::Result<()> {
let paths = ["audit-1.log", "audit-2.log", "audit-3.log", "audit-4.log"];
let total = thread::scope(|scope| -> io::Result<usize> {
let handles: Vec<_> = paths.chunks(2).map(|chunk| {
scope.spawn(move || -> io::Result<usize> {
let mut subtotal = 0;
for path in chunk {
subtotal += count(path)?;
}
Ok(subtotal)
})
}).collect();
let mut total = 0;
for handle in handles {
total += handle.join()
.map_err(|_| io::Error::other("worker panicked"))??;
}
Ok(total)
})?;
println!("errors={total}");
Ok(())
}
Complexity signal: Python and Java provide convenient executor APIs. Go exposes queue ownership and shutdown. Rust’s scoped threads can borrow local data without requiring global lifetime or shared ownership. For an unbounded input stream, also bound queued work and retained results; a two-thread executor alone does not bound its submission queue. Add cancellation/deadlines when adapting this to remote I/O.
8. Reserve Stock Without a Check-Then-Act Race
Contract: start with ten units and attempt twenty concurrent one-unit reservations. Exactly ten succeed, stock never goes negative, and the program prints accepted=10 remaining=0.
The invariant is the comparison, not equal thread counts: Java uses virtual threads, Python a four-thread pool, Go goroutines, and Rust scoped OS threads. This is an in-process example, not a distributed inventory lock.
Java
import java.util.ArrayList;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
public class Main {
static boolean reserve(AtomicInteger stock) {
while (true) {
int available = stock.get();
if (available == 0) return false;
if (stock.compareAndSet(available, available - 1)) return true;
}
}
public static void main(String[] args) throws Exception {
var stock = new AtomicInteger(10);
int accepted = 0;
try (var pool = Executors.newVirtualThreadPerTaskExecutor()) {
var results = new ArrayList<Future<Boolean>>();
for (int i = 0; i < 20; i++) {
results.add(pool.submit(() -> reserve(stock)));
}
for (var result : results) {
if (result.get()) accepted++;
}
}
System.out.println("accepted=" + accepted + " remaining=" + stock.get());
}
}
Python
from concurrent.futures import ThreadPoolExecutor
from threading import Lock
class Inventory:
def __init__(self) -> None:
self.stock = 10
self.lock = Lock()
def reserve(self, _: int) -> bool:
with self.lock:
if self.stock == 0:
return False
self.stock -= 1
return True
inventory = Inventory()
with ThreadPoolExecutor(max_workers=4) as pool:
accepted = sum(pool.map(inventory.reserve, range(20)))
print(f"accepted={accepted} remaining={inventory.stock}")
Go
package main
import (
"fmt"
"sync"
)
func main() {
stock := 10
var mutex sync.Mutex
var workers sync.WaitGroup
results := make(chan bool, 20)
for i := 0; i < 20; i++ {
workers.Add(1)
go func() {
defer workers.Done()
mutex.Lock()
accepted := stock > 0
if accepted {
stock--
}
mutex.Unlock()
results <- accepted
}()
}
workers.Wait()
close(results)
accepted := 0
for success := range results {
if success {
accepted++
}
}
fmt.Printf("accepted=%d remaining=%d\n", accepted, stock)
}
Rust
use std::sync::Mutex;
use std::thread;
fn main() {
let stock = Mutex::new(10);
let accepted = thread::scope(|scope| {
let handles: Vec<_> = (0..20).map(|_| {
scope.spawn(|| {
let mut available = stock.lock().expect("stock lock poisoned");
if *available == 0 {
return false;
}
*available -= 1;
true
})
}).collect();
handles.into_iter()
.map(|handle| usize::from(handle.join().expect("worker panicked")))
.sum::<usize>()
});
let remaining = *stock.lock().expect("stock lock poisoned");
println!("accepted={accepted} remaining={remaining}");
}
Complexity signal: Java’s compare-and-set makes one conditional decrement atomic; the other examples protect the whole operation with a lock. The Python lock remains necessary even on a GIL-enabled build. Rust’s type system enforces synchronized sharing, but developers must still choose the correct business-critical section. Across service instances, use a transactional conditional database update and check the affected-row count.
9. Retry Only an Explicitly Transient Failure
Contract: an injected operation returns HTTP-like status codes. Retry only 503, at most three calls, waiting 50 ms then 100 ms. Return success only for 200. Any other status fails immediately; exhausting 503 responses fails.
The deterministic fake upstream returns 503, 503, 200, so each program prints ok. These are retry-policy programs, not HTTP clients. A real HTTP adapter must set request timeouts, validate responses, and propagate transport errors.
Java
import java.util.function.IntSupplier;
import java.util.stream.IntStream;
public class Main {
static void retry(IntSupplier send) throws InterruptedException {
for (int attempt = 0; attempt < 3; attempt++) {
int status = send.getAsInt();
if (status == 200) return;
if (status != 503) {
throw new IllegalStateException("Non-retryable HTTP " + status);
}
if (attempt < 2) Thread.sleep(50L << attempt);
}
throw new IllegalStateException("Retry budget exhausted");
}
public static void main(String[] args) throws InterruptedException {
var statuses = IntStream.of(503, 503, 200).iterator();
retry(statuses::nextInt);
System.out.println("ok");
}
}
Python
from collections.abc import Callable
import time
def retry(send: Callable[[], int]) -> None:
for attempt in range(3):
status = send()
if status == 200:
return
if status != 503:
raise RuntimeError(f"Non-retryable HTTP {status}")
if attempt < 2:
time.sleep(0.05 * (2 ** attempt))
raise RuntimeError("Retry budget exhausted")
statuses = iter([503, 503, 200])
retry(lambda: next(statuses))
print("ok")
Go
package main
import (
"fmt"
"log"
"time"
)
func retry(send func() int) error {
for attempt := 0; attempt < 3; attempt++ {
status := send()
if status == 200 {
return nil
}
if status != 503 {
return fmt.Errorf("non-retryable HTTP %d", status)
}
if attempt < 2 {
time.Sleep((50 * time.Millisecond) << attempt)
}
}
return fmt.Errorf("retry budget exhausted")
}
func main() {
statuses := []int{503, 503, 200}
index := 0
err := retry(func() int {
status := statuses[index]
index++
return status
})
if err != nil {
log.Fatal(err)
}
fmt.Println("ok")
}
Rust
use std::{thread, time::Duration};
fn retry(mut send: impl FnMut() -> u16) -> Result<(), String> {
for attempt in 0..3 {
let status = send();
if status == 200 {
return Ok(());
}
if status != 503 {
return Err(format!("non-retryable HTTP {status}"));
}
if attempt < 2 {
thread::sleep(Duration::from_millis(50 * (1 << attempt)));
}
}
Err("retry budget exhausted".to_owned())
}
fn main() -> Result<(), String> {
let mut statuses = [503, 503, 200].into_iter();
retry(|| statuses.next().expect("fake upstream exhausted"))?;
println!("ok");
Ok(())
}
Complexity signal: passing behavior is straightforward in all four languages; Rust explicitly identifies a closure that mutates captured state. Production retries need an overall deadline, cancellation-aware waits, jitter, and a policy for Retry-After. The 150 ms here bounds only sleeping, not the total operation. Retry writes only with an idempotency design; blindly replaying payments can duplicate charges.
10. Publish a Report Without Exposing a Partial File
Contract: write the same fixed CSV report to a temporary file in the destination directory, close it, then replace report.csv. Print published only after replacement succeeds. Remove a temporary file left by failure.
This example assumes a private working directory on a local Linux filesystem, one writer, and same-filesystem rename semantics. It demonstrates atomic visibility, not crash durability: durable publication also needs file and directory synchronization. Network filesystems and Windows need their own guarantees checked. Existing destination permissions may be replaced by the temporary file’s permissions.
Java
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
public class Main {
public static void main(String[] args) throws Exception {
var target = Path.of("report.csv").toAbsolutePath();
var temporary = Files.createTempFile(target.getParent(), ".report-", ".tmp");
try {
Files.writeString(temporary,
"customer,cents\nalice,5000\nbob,2500\n", StandardCharsets.UTF_8);
Files.move(temporary, target,
StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
} finally {
Files.deleteIfExists(temporary);
}
System.out.println("published");
}
}
Python
import os
from pathlib import Path
import tempfile
descriptor, name = tempfile.mkstemp(prefix=".report-", suffix=".tmp", dir=".")
temporary = Path(name)
try:
with os.fdopen(descriptor, "w", encoding="utf-8", newline="") as output:
output.write("customer,cents\nalice,5000\nbob,2500\n")
os.replace(temporary, "report.csv")
finally:
temporary.unlink(missing_ok=True)
print("published")
Go
package main
import (
"errors"
"fmt"
"log"
"os"
)
func publish() (err error) {
file, err := os.CreateTemp(".", ".report-*.tmp")
if err != nil {
return err
}
temporary := file.Name()
defer func() {
cleanupErr := os.Remove(temporary)
if cleanupErr != nil && !errors.Is(cleanupErr, os.ErrNotExist) {
err = errors.Join(err, cleanupErr)
}
}()
if _, err := file.WriteString("customer,cents\nalice,5000\nbob,2500\n"); err != nil {
return errors.Join(err, file.Close())
}
if err := file.Close(); err != nil {
return err
}
return os.Rename(temporary, "report.csv")
}
func main() {
if err := publish(); err != nil {
log.Fatal(err)
}
fmt.Println("published")
}
Rust
use std::fs::{self, OpenOptions};
use std::io::{self, Write};
use std::os::unix::fs::OpenOptionsExt;
fn main() -> io::Result<()> {
let temporary = format!(".report-{}.tmp", std::process::id());
let mut file = OpenOptions::new()
.write(true).create_new(true).mode(0o600).open(&temporary)?;
let result = (|| -> io::Result<()> {
file.write_all(b"customer,cents\nalice,5000\nbob,2500\n")?;
drop(file);
fs::rename(&temporary, "report.csv")
})();
match fs::remove_file(&temporary) {
Ok(()) => {}
Err(error) if error.kind() == io::ErrorKind::NotFound => {}
Err(error) => return Err(error),
}
result?;
println!("published");
Ok(())
}
Complexity signal: Java and Python expose concise filesystem operations; Go makes cleanup errors explicit. Rust’s standard library requires more manual temporary-file management here; a maintained tempfile crate would usually be preferable in an application. Its exclusive create_new refuses a stale-name collision instead of overwriting another file. Java intentionally fails if atomic moves are unsupported rather than silently falling back to a non-atomic copy.
Correctness Checklist: Make the Differences Observable
Use the same assertions in every language. Happy-path output alone does not establish correctness.
| Scenario | Expected success | Important negative or boundary case |
|---|---|---|
| 1. Validation | BOOK-1:2 | Reject empty/lowercase SKU, Unicode digits, 0, 11, and extra characters; accept 01 |
| 2. Money | 6492 | A one-cent subtotal at 50% tax totals two cents; reject negative input and out-of-range quantities |
| 3. State machine | state-ok | Reject shipping a new order and changing any terminal order |
| 4. Aggregation | alice=5000, then bob=2500 | Unpaid rows contribute nothing; ordering must not depend on hash iteration |
| 5. Import | Two validated lines | A malformed second row reports row 2; a missing file fails; CRLF works |
| 6. Deduplication | evt-2,evt-1,evt-3 | Preserve first occurrence, not alphabetical order; empty batch produces no IDs |
| 7. Bounded work | errors=4 | A missing audit file fails instead of producing a partial success; never exceed two readers |
| 8. Stock | accepted=10 remaining=0 | Repeat under contention; no successful reservation once stock reaches zero |
| 9. Retry | ok after three calls | 400 fails after one call; three 503s fail without a fourth call |
| 10. Publication | published, complete CSV | Existing report is replaced whole; a rename failure leaves no newly created temporary file |
Turn these into your project’s normal test framework: JUnit, Python unittest/pytest, Go’s testing, or Rust’s built-in tests. For the Go concurrency examples, go run -race main.go exercises the race detector on that program; it is not proof that every possible execution is race-free.
Do not use source-line counts as a universal productivity metric. These examples include imports, entrypoints, and explicit failure handling. A framework, a reusable domain library, or a different API can change the count without changing the business problem.
Which Language Would I Choose?
| Situation | Strong starting choice | Reason and tradeoff |
|---|---|---|
| Existing Java estate, complex transactions, many integrations | Java | Reuse operational knowledge and libraries; manage framework and JVM complexity |
| AI/data workflow or rapidly evolving internal product | Python | Leverage ecosystem and iteration speed; profile hot paths and enforce types/contracts at boundaries |
| Small network service, infrastructure agent, or distributable CLI | Go | Straightforward concurrency and delivery; design cancellation and error handling carefully |
| Memory-sensitive parser, systems component, or latency-critical native code | Rust | Strong memory-safety guarantees and control; budget learning and implementation time |
| Mostly database-bound business API | Existing team stack | Query plans, indexes, connection limits, and transaction design may dominate language overhead |
A mixed system can be sensible: Python orchestration, Java business services, Go infrastructure tools, and a Rust native component. But each language adds build, debugging, patching, and hiring obligations. Introduce another runtime only when a measured benefit pays for that complexity.
The veterans still deliver. The challengers solve real problems. Choose the constraints and failure modes you understand, not the age of the language.
Official References
- OpenJDK release history and JDK 27.
- Python 3.15 release schedule and What’s New in Python 3.15.
- JEP 444: Virtual Threads and Java Files API.
- Python free-threading guidance,
concurrent.futures, andos.replace. - Go GC guide, memory model, race detector, and
os.Rename. - Rust ownership, shared-state concurrency, unsafe Rust, and
std::fs::rename.