Old-School Backend / 2026 ・ No.01

Old-School Backend

Part 1: The big picture — how one submission travels through the server into the database

Published

The Wireshark window, recorded at the expense server's port (192.168.1.20). Packet 4 is the POST /api/expenses from the PC, packets 6 to 16 are PGSQL exchanges with the database server (192.168.1.30), and 19 is HTTP/1.1 201. Packet 13 is selected; its PostgreSQL details (Type: Parse) show Query: insert into expense (id, applied_on, item, amount, status) values ($1, $2, $3, $4, 'submitted') and Parameters: 4
What lies behind one submission. Having received it from the PC (4), the expense server goes back and forth with the database server four times before writing its reply (19). Packet 13, selected, is the SQL that writes the submission into the table (Recorded on 26 October 2026 on a practice network the author built in the cloud and opened in Wireshark 4.2.2. The bar across the top of the window and the headings were added for the illustration.)

The frontend series built the submission form, and the networking series read that submission line by line as it crossed the wire. Now it's time for the receiving end: the backend. The language is Java, over a planned fifteen parts.
Part 1 is the big picture. We run an expense server written in Java with Spring Boot and a PostgreSQL database server on a practice network, and measure how one submission travels through the server and into the database.

1

To the receiving end

AraiThe journey along the wire is done. From today we're inside the server!

FuruiThat's right. The POST /api/expenses we've been sending, this time we receive it ourselves.

AraiSpring Boot, right? Add a few annotations and it runs. An AI could write it in a second.

FuruiIt runs in a second, and in a second you've lost track of what's happening. So first we draw the big picture, then open up what's moving behind it, one piece at a time.

Route map of all 15 parts (you are here: part 1)

1 Big picture 2 Java basics 3 Raw HTTP 4 Raw JDBC 5 Spring Boot 6 Validation 7 Errors 8 DB access 9 Transactions 10 Workflow 11 Auth 12 Files 13 Logging 14 Testing 15 Running it You are here Basics By hand Front door Data Rules & safety Operations
  • The big picture — one submission passing through eight pairs of hands inside the server and into the database (measured, with Wireshark)
  • Doing it by hand — parts 3 and 4 handle HTTP and the database without a framework, to see what Spring does for us
  • Business-system staples — validation, error replies, transactions, approval flows, authentication, files, logging, testing
  • Measured every time — run on a practice server and explained with real logs, SQL, packets and timings

The subject is the same expense system as before. The submission sent from the page in part 10 of the frontend series and read line by line in part 10 of the networking series is now received in Java and stored in a database.

2

A server is a program that waits

The practice network has three machines: the PC (192.168.1.10), the expense server (192.168.1.20, named expenses.example) and the database server (192.168.1.30). On the expense server we started the Java program written for this part.

# Start the Java program on the expense server (192.168.1.20)
app$ java -jar expenses-0.0.1-SNAPSHOT.jar
    … Tomcat initialized with port 8080 (http)
    … HikariPool-1 - Start completed.
    … Started ExpensesApplication in 3.183 seconds
# Who is waiting, and where
app$ ss -ltnp 'sport = :8080'
    LISTEN  0.0.0.0:8080   users:(("java",pid=760,fd=18))
# Connections to the DB (already open at startup)
app$ ss -tn 'dport = :5432' | wc -l
    10
# How big the program is
app$ ls -l expenses-0.0.1-SNAPSHOT.jar ; grep VmRSS /proc/760/status ; ls /proc/760/task | wc -l
    24,319,744 bytes
    VmRSS:  200876 kB
    38

A “server” isn't a special machine. It's a program that opens a port and waits for requests. ss shows java waiting on port 8080 of 0.0.0.0 (networking part 11's “wait at every door”). And by the time it has started, it has already opened 10 connections to port 5432 on the database server. Chapter 7 explains why.

Startup took 3.2 seconds, about 200 MB of memory and 38 threads. Even a small expense server has a fair amount of work going on inside.

3

One submission's trip

From the PC we sent the same submission as in the networking series.

# From the PC, the same submission as in part 10
pc$ curl -D - -X POST -H 'Content-Type: application/json' \
       -d '{"date":"2026-10-26","item":"交通費","amount":1280}' \
       http://expenses.example:8080/api/expenses
    HTTP/1.1 201
    Location: /api/expenses/EXP-2026-0012
    Content-Type: application/json
    
    {"id":"EXP-2026-0012","status":"submitted"}     (0.281 s)
# Five more of the same
    201 (0.009 s)  201 (0.010 s)  201 (0.007 s)  201 (0.005 s)  201 (0.005 s)

Back came 201 Created and a reference number, also given in Location (networking part 10). What happened inside the expense server meanwhile, we had it write to its log (the log messages are in Japanese, as in the program).

# The expense server's log (2nd submission; every line is thread http-nio-8080-exec-2)
    15:47:44.473 ExpenseController      窓口:申請を受け取った ExpenseRequest[…, amount=1280]
    15:47:44.473 JdbcTransactionManager Creating new transaction with name [example.expenses.ExpenseService.submit]
    15:47:44.473 JdbcTransactionManager Acquired Connection […] for JDBC transaction
    15:47:44.474 JdbcTemplate           Executing prepared SQL statement [select nextval('expense_no')]
    15:47:44.474 ExpenseService         担当者:受付番号を決めた EXP-2026-0013
    15:47:44.474 JdbcTemplate           Executing prepared SQL statement [insert into expense (…) values (?, ?, ?, ?, 'submitted')]
    15:47:44.475 ExpenseRepository      倉庫番:1行書いた EXP-2026-0013
    15:47:44.475 JdbcTransactionManager Committing JDBC transaction on Connection […]
    15:47:44.476 JdbcTransactionManager Releasing JDBC Connection […] after transaction
    15:47:44.476 ExpenseController      窓口:201 を返す EXP-2026-0013

One submission passes through eight pairs of hands inside the server

receive Tomcat read JSON Jackson check Validator counter Controller clerk Service storekeeper Repository database PostgreSQL reply 201 Jackson, Tomcat 3.3 ms before the DB 2.5 ms 4 DB round trips 1.6 ms writing the reply One warmed-up submission (from Wireshark times, 7.4 ms in all) A submission passes through eight pairs of hands; we wrote three The rest is framework and libraries; this series opens them one by one

Tomcat receives the HTTP, Jackson turns the JSON into Java values, and Hibernate Validator checks them. Only then is the “counter” we wrote called. The counter hands it to the “clerk”, who starts a transaction and assigns a reference number, and the “storekeeper” writes one row to the table with SQL. The transaction is committed, the connection goes back to the pool, and the counter returns 201. By the log's clock, 3 milliseconds from receipt to reply.

Split by Wireshark's clock: 3.3 ms from the request arriving to the first DB message, 2.5 ms for four round trips to the DB, and 1.6 ms to write and send the reply.

4

Counter, clerk, storekeeper

The part we wrote is split into three roles. Here is the actual code (comments and log messages are in Japanese).

// The counter (Controller): takes the HTTP request, hands it on, writes the 201
@PostMapping
public ResponseEntity<Map<String, String>> submit(@Valid @RequestBody ExpenseRequest req) {
    log.info("窓口:申請を受け取った {}", req);
    String id = service.submit(req);
    log.info("窓口:201 を返す {}", id);
    return ResponseEntity.created(URI.create("/api/expenses/" + id))
            .body(Map.of("id", id, "status", "submitted"));
}

// The clerk (Service): the business rules; from here on it's one transaction
@Transactional
public String submit(ExpenseRequest req) {
    String id = "EXP-2026-%04d".formatted(repo.nextNumber());
    log.info("担当者:受付番号を決めた {}", id);
    repo.insert(id, req);
    return id;
}

// The storekeeper (Repository): writes SQL and talks to the DB
void insert(String id, ExpenseRequest r) {
    jdbc.sql("insert into expense (id, applied_on, item, amount, status) values (?, ?, ?, ?, 'submitted')")
        .params(id, r.date(), r.item(), r.amount())
        .update();
    log.info("倉庫番:1行書いた {}", id);
}

One class per role

counter Controller takes HTTP writes the reply 31 lines clerk Service business rules transaction 26 lines storekeeper Repository writes SQL talks to the DB 28 lines DB PostgreSQL HTTP → One class per role: the counter knows no SQL, the storekeeper no HTTP Line counts are this part's actual files, blank lines excluded

The counter (Controller) knows only HTTP, the clerk (Service) only the business rules, the storekeeper (Repository) only SQL. Putting SQL in the counter would work, but as approval flows and permission checks pile up, one class becomes unmanageable. With roles separated, “change the database” touches only the storekeeper, and “change the spending limit” only the clerk.

AraiSame idea as network layers. The upper layer doesn't need to know what's inside the lower one.

FuruiExactly. The old merchants kept the counting room, the kitchen and the storehouse apart too.

5

The database is another program

The database isn't a component inside the expense server. It's a separate program running on a separate server, connected to the expense server over TCP port 5432. The cover picture was recorded at the expense server's port: between the PC's submission (4) and the reply (19) sit the exchanges with the database server (6 to 16).

There were four round trips. The first “empty query” checks that the connection borrowed from the pool is still alive. Next come BEGIN (start of the transaction) and fetching the reference number, then the INSERT, and finally COMMIT (make it final). Part 9 covers transactions in depth.

The Wireshark window: the same packet 13, with the second PostgreSQL message (Type: Bind) expanded. Parameter formats: 4, Parameter values: 4. The bytes pane on the right shows the text EXP-2026-0012 and 2026-10-26
The second half of packet 13. Separately from the SQL statement (Parse), the values for $1 to $4 (the reference number, date, item and amount) travel in a Bind message. Keeping statement and values apart is the heart of defending against SQL injection, which part 4 covers (Recorded on 26 October 2026 on a practice network the author built in the cloud and opened in Wireshark 4.2.2. The bar across the top of the window and the headings were added for the illustration.)

Notice that the SQL statement and its values travel separately. The statement keeps blanks, $1, $2, $3, $4, and the reference number, date, item and amount that fill them arrive in a separate box called Bind. That way, even if the item contains a malicious string, it is never read as part of the SQL statement. In part 4 we'll deliberately mix statement and values and watch it break.

# Look inside the table on the DB server
db$ psql -d expenses -c 'select id, applied_on, item, amount, status from expense order by id limit 3'
          id       | applied_on |  item  | amount  |  status
    ---------------+------------+--------+---------+-----------
     EXP-2026-0012 | 2026-10-26 | 交通費 | 1280.00 | submitted
     EXP-2026-0013 | 2026-10-26 | 交通費 | 1280.00 | submitted
     EXP-2026-0014 | 2026-10-26 | 交通費 | 1280.00 | submitted

On the database server, the table indeed holds one row per submission. The amount is 1280.00: the 1280 sent from the page has become a number with two decimal places. How to hold money in Java is the big topic next time.

6

The first one is slow

Chapter 3's submission took 0.281 seconds by curl's clock. The next five took 0.005 to 0.010 seconds.

Two Wireshark packet lists stacked, each showing only packets with a payload. Top, the first submission: POST at 0.000083 s, first PGSQL at 0.219919 s, 201 at 0.280707 s. Bottom, the 57th, once warmed up: POST at 0.000116 s, first PGSQL at 0.003453 s, 201 at 0.008110 s
The same submission takes 0.28 s as the first after startup and 0.008 s once warmed up. Almost all the difference comes before the first DB message (preparing the JSON reader and the validator) (Recorded on 26 October 2026 on a practice network the author built in the cloud and opened in Wireshark 4.2.2. The bar across the top of the window and the headings were added for the illustration.)

Almost all the difference comes before the first DB message: 0.22 s for the first submission, 0.003 s once warmed up. The log shows the first one spent about 0.15 s before reaching the counter. On first use, the JSON reader and the validator get themselves ready, Java classes are loaded, and the JVM gradually compiles the busiest paths into faster machine code.

POINT

A Java server is “slowest right after startup”. When measuring performance, throw away the first few requests. The same reason explains slow responses just after a production deployment (part 15).

7

Many at once

At month end, submissions pile up before accounting's deadline. We sent 50 at once from the PC.

# Send 50 submissions at once from the PC
pc$ python3 par.py
    status {201: 50}
    total 0.157 s  slowest 0.135 s  fastest 0.030 s
# Count the thread names that received them in the server log
app$ grep … app.log | (thread names) | sort -u | wc -l
    36
# Threads in the Java program (38 right after startup)
app$ ls /proc/760/task | wc -l
    66

One thread per request; DB connections are borrowed from a pool

50 at once Tomcat threads 36 took part conn. pool HikariCP: 10 DB One thread per request; 10 DB connections, opened ahead, are reused All 50 got 201 in 0.157 s. Not reconnecting each time: networking part 6 The thread limit (default 200) and pool size (default 10) come in part 15

All 50 got 201 within 0.157 seconds, and the log shows 36 different threads receiving them. Tomcat assigns each incoming request to a thread (a worker inside the program that can run at the same time as others). Database connections, on the other hand, are the 10 opened at startup, reused (a connection pool). It saves starting over with the TCP greeting each time, exactly networking part 6's “reusing connections”.

With 36 workers sharing 10 phones, some wait for a free line. Thread counts, pool sizes and virtual threads (since Java 21) are measured in part 15.

8

The toolkit

This expense server is a template made with Spring Initializr (start.spring.io), plus five Java files, a table definition (schema.sql) and a few changed settings. Building it produces a single jar file.

Inside the jar: this part's 112 lines of Java, and 48 libraries

expenses-0.0.1-SNAPSHOT.jar (24.3 MB, 48 libraries) Spring Boot 4.1.2 assembly Tomcat 11.0 takes HTTP Jackson 3.1 JSON Hibernate Validator 9.1 validation HikariCP 7.0 connection pool PostgreSQL JDBC 42.7 talks to DB This part's Java: 6 files, 112 lines (one from the template) JVM (OpenJDK 25) These 112 lines stand on a great deal of other craftspeople's work
ToolJobVersion used here
JDK (OpenJDK)compiles Java and runs it on the JVM25.0.4
Mavengathers libraries and buildsthe template's mvnw
Spring Bootassembles libraries, saves configuration work4.1.2
Tomcattakes HTTP (bundled with Spring Boot)11.0
PostgreSQLthe database server16

112 lines of Java, counting the one file from the template. Receiving HTTP, reading and writing JSON, validation and the connection pool were all done by libraries. That's a good thing in itself. But without knowing what the libraries do, you're helpless when they stop working. So this series deliberately handles HTTP and the database by hand in parts 3 and 4, and comes back to Spring Boot in part 5.

9

The backend's standard jobs

Today's expense server only “receives and stores”. A business system's backend has other standard jobs too. Here they are in the order this series covers them.

JobWhat to watchPart
Holding values correctlyno floating point for money; dates and times, and “whose time”2
Validationchecks in the page are courtesy; the server is the guard6
Error repliesstatus codes, and error bodies people can read7
Reading and writing the DBSQL injection, code that slows down as data grows (N+1)4, 8
Transactionshow far to roll back on failure; simultaneous updates9
Approval flowwhere to enforce submitted → approved → returned10
Authentication and permissionsholes that show other people's submissions; storing passwords11
Filesreceipt uploads, CSV and Excel statements12
Logging and monitoringrecords you can trace later, without personal data13
Testingautomatic checks after every change14
Keeping it runningdeployment, performance, library updates15

There are two well-known ways to read and write the database: JPA and MyBatis. Part 8 tries both on the same submission and compares them. Until then we use the plain approach of writing SQL ourselves (Spring's JdbcClient).

10

Getting your own PC ready

So you can follow along from next time, gather the tools now.

WhatHow, for exampleCheck
JDK 25install a distribution such as Eclipse Temurinjava -version
EditorIntelliJ IDEA, or VS Code with the Java extensionscan open a new project
PostgreSQLeasiest with Dockerdocker run -d -p 5432:5432 -e POSTGRES_PASSWORD=practice postgres:16
Templateon start.spring.io, pick Spring Web, JDBC API, PostgreSQL Driver and Validation./mvnw spring-boot:run
curlalready on Mac, Windows and Linuxcurl --version

Maven is set up automatically by the template's mvnw (mvnw.cmd on Windows), so there's no need to install it separately.

§ — On one page, and next time

  1. A server is a program that opens a port and waits: 8080 for the expense server, 5432 for the database server.
  2. One submission passes Tomcat → Jackson → Validator → counter → clerk → storekeeper → DB → reply: eight pairs of hands, 7.4 ms once warmed up.
  3. We write the counter, clerk and storekeeper, one class per role.
  4. The database is another program. SQL statements and values travel separately, wrapped in BEGIN and COMMIT.
  5. A Java server's first request is slow (0.28 s → 0.008 s).
  6. Requests arriving together get a thread each; DB connections are borrowed from a pool and reused.
  7. This part's 112 lines of Java stand on 48 libraries. To see inside, parts 3 and 4 do it by hand.

AraiSo many people involved in one submission.

FuruiHomework, then. We sent a date of the 26th, but the reply's Date header said the 25th, the log said a little after 15:00, and the database's log said a little after midnight. Which is right?

Arai…All of them?

FuruiNext time, Java basics, starting with holding money, dates and times correctly.

References

The recordings were made on 26 October 2026 on a practice network the author built with network namespaces on Linux (Ubuntu 24.04, kernel 6.18) in the cloud. The expense server is OpenJDK 25.0.4 with Spring Boot 4.1.2 (a Spring Initializr template plus this part's code, talking to the database through Spring's JdbcClient); the database server is PostgreSQL 16; the PC side uses curl 8.5.0 and Python 3. All names and addresses are made up for the practice. Log times are left as the expense server had them (UTC). Every number and output in the text is a real value read from those recordings and the commands' output (long outputs are trimmed). The characters are fictional.

Old-School Backend, part 1 — The big picture / 2026.10

← Back to all posts