- Chapter
- Chapter 1 — Database Fundamentals
- Prerequisites
- Basic Java and SQL familiarity
- You will be able to
- Explain the five needs databases satisfy, contrast files with databases, relate each need to the DBMS mechanism that provides it (schema, constraints, transactions, MVCC/locking, WAL), and judge when application code should — and should not — re-implement database guarantees
- Covers
- Persistent data; Structured data; Data sharing; Concurrent access; Durability
On this page
- 1. Concept / Definition
- 2. Why It Exists
- 3. Core Concepts
- 4. How It Works Internally — From COMMIT to Disk
- 4A. Deep Dive — What Applications Should Not Re-implement
- 5. APIs / Features / Syntax
- 6. Practical Examples
- 7. Common Problems & Mistakes
- 8. Performance & Production Considerations
- 9. Architecture & Design Considerations
- 10. Interview Questions & Answers
1Concept / Definition
A database is an organised collection of data managed by a database management system (DBMS) that provides persistent storage, a declared structure, shared access for many users and applications, safe concurrent updates and durability of committed changes.

| Need | DBMS mechanism |
|---|---|
| Persistent data | Data files on durable storage, independent of application processes |
| Structured data | Schema: tables, types, constraints |
| Data sharing | Server accessed by many clients through SQL and access control |
| Concurrent access | Transactions, locking, MVCC, isolation levels |
| Durability | Write-ahead logging, fsync, crash recovery, replication, backups |
2Why It Exists
Before DBMSs, each program stored data in its own files with its own format. Sharing data meant copying files; two programs updating the same file corrupted it; a crash mid-write left files half-written; answering a new question meant writing a new program. Hierarchical (IMS, 1966) and network (CODASYL) databases centralised data but tied programs to physical access paths. Codd's relational model (1970) and SQL separated what data is needed from how it is retrieved, and transaction theory (Gray, 1970s) formalised the ACID guarantees that business systems rely on.

3Core Concepts
3.1 Persistent data
Persistence means data outlives the process that created it — across restarts, deployments and hardware changes. In Java, objects vanish when the JVM exits; the database is where state lives between requests and releases.
| Persistence concern | Database answer |
|---|---|
| Survive application restarts | Data in database files, not heap |
| Survive redeployments and scaling | Stateless app instances share one database |
| Long retention (years) | Archiving, partitioning, backups |
| Independent of application language | SQL and data outlive the Java codebase |
3.2 Structured data
A schema declares tables, column types and constraints. The database rejects data that violates it — no matter which application writes.
CREATE TABLE customer (
customer_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email VARCHAR(254) NOT NULL UNIQUE,
full_name VARCHAR(200) NOT NULL,
pan CHAR(10) CHECK (pan ~ '^[A-Z]{5}[0-9]{4}[A-Z]$'),
credit_limit NUMERIC(12,2) NOT NULL DEFAULT 0 CHECK (credit_limit >= 0),
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);| Benefit of structure | Example |
|---|---|
| Validity | Negative credit limits impossible |
| Shared understanding | Every consumer sees the same types |
| Efficient storage & indexing | Fixed types allow compact pages and B-trees |
| Queryability | Structure enables SQL joins, filters, aggregates |
Schema-less stores move structure into application code ("schema-on-read"); the structure still exists — it is just not enforced centrally.
3.3 Data sharing
One database commonly serves many consumers: web and mobile back ends, batch jobs, reporting tools, integrations and admin consoles.
| Sharing mechanism | Purpose |
|---|---|
| SQL as a common language | Any client can query |
Roles and privileges (GRANT) | Control who reads/writes what |
| Views | Stable, restricted interfaces over tables |
| Replication | Share load with read replicas and other regions |
Architecture note: sharing one database across services creates tight coupling (Book 5, Volume VII, 8.3). Sharing within one application or through read-only reporting replicas is normal; sharing writable tables across independently deployed services is an anti-pattern.
3.4 Concurrent access

Many clients read and write simultaneously. The DBMS ensures each transaction sees a consistent view and that concurrent updates don't corrupt data.
-- Two sessions buy the last unit simultaneously;
-- the DBMS serialises the conflicting updates
UPDATE inventory SET available = available - 1
WHERE sku = 'A1' AND available > 0;
-- second session waits for the first, then updates 0 rows// Spring: one transaction per use case; the database does the concurrency control
@Transactional
public boolean reserve(Sku sku) {
int updated = jdbc.sql("""
UPDATE inventory SET available = available - 1
WHERE sku = :sku AND available > 0""")
.param("sku", sku.value())
.update();
return updated == 1;
}| Mechanism | What it does |
|---|---|
| Transactions | Group statements into atomic units |
| Row locks | Serialize conflicting writes |
| MVCC (PostgreSQL, InnoDB) | Readers see snapshots and don't block writers |
| Isolation levels | Control which anomalies are allowed (Volume II) |
| Constraints under concurrency | UNIQUE/FK checked atomically — no race windows |
3.5 Durability

Durability (the D in ACID) guarantees that once a transaction commits, its changes survive crashes and power loss.
| Layer of durability | Protects against |
|---|---|
| WAL + fsync on commit | Process or OS crash, power loss |
| Crash recovery (WAL replay) | Restores committed changes after restart |
| Synchronous replication | Loss of a single server/disk |
| Backups + WAL archiving (PITR) | Human error, corruption, disaster |
| Multi-region replicas | Regional outages |
4How It Works Internally — From COMMIT to Disk
1. The application sends statements within a transaction; the server modifies pages in the shared buffer pool (memory).
2. Each change generates a WAL record appended to the log buffer.
3. On COMMIT, the WAL is flushed (fsync) to disk; only then does the server acknowledge success.
4. Modified data pages are written to data files later by the background writer and checkpoints.
5. After a crash, recovery replays WAL from the last checkpoint, redoing committed changes; uncommitted changes are never visible.
| Setting (PostgreSQL) | Effect | Trade-off |
|---|---|---|
synchronous_commit = on (default) | Commit waits for local WAL flush | Durability vs latency |
synchronous_commit = off | Commit returns before flush | Faster; may lose last ~few hundred ms on crash |
synchronous_standby_names | Wait for replica acknowledgment | Survives server loss; higher latency |
full_page_writes = on | Protects against torn pages | More WAL volume |
4ADeep Dive — What Applications Should Not Re-implement
| Temptation in application code | Why it fails | Use instead |
|---|---|---|
| Check-then-insert for uniqueness | Race between check and insert | UNIQUE constraint; handle violation |
| In-memory locks for shared rows | Doesn't work across instances | Row locks, conditional updates, optimistic versions |
| Writing "before" and "after" files for safety | Crash windows, complexity | Transactions |
| Caching as the source of truth | Lost on restart, inconsistent | Database as system of record; cache as copy |
| Manual referential checks | Races, missed paths (batch, admin) | Foreign keys |
// Correct: let the UNIQUE constraint decide; translate the violation
public CustomerId register(NewCustomer c) {
try {
return new CustomerId(jdbc.sql("""
INSERT INTO customer (email, full_name) VALUES (:email, :name)
RETURNING customer_id""")
.param("email", c.email()).param("name", c.name())
.query(Long.class).single());
} catch (DuplicateKeyException e) {
throw new EmailAlreadyRegistered(c.email());
}
}5APIs / Features / Syntax
| Feature | Purpose |
|---|---|
CREATE TABLE … NOT NULL / UNIQUE / CHECK / REFERENCES | Structure and integrity |
BEGIN … COMMIT / ROLLBACK | Transactions |
GRANT / REVOKE, roles, row-level security | Controlled sharing |
JDBC Connection.setAutoCommit(false), commit() | Transactions in Java |
Spring @Transactional, TransactionTemplate | Declarative/programmatic transactions |
Spring JdbcClient, Spring Data JPA/JDBC | Data access |
| HikariCP | Connection pooling for shared access |
6Practical Examples
6.1 Replacing a JSON file store
A configuration service stored tenant settings in JSON files on a shared volume; concurrent edits overwrote each other. Moving to a tenant_setting table with optimistic locking removed lost updates and enabled auditing.
6.2 Durability misunderstanding
A team disabled synchronous_commit to speed up an order service; a crash lost 40 committed orders. The setting was restored for orders and kept off only for a click-stream table where small loss was acceptable.
6.3 Worked example — mapping requirements to database guarantees
Scenario. A clinic chain in Pune is replacing a spreadsheet-and-phone booking process with an appointment system used by receptionists, doctors and an SMS reminder service. The product owner's requirements are written in business language; the architect's job is to turn each one into a guarantee the database must provide, rather than leaving every rule to application code.
Requirements for a clinic appointment system:
| Requirement | Need (1.1) | Database mechanism |
|---|---|---|
| Appointments must exist after server restarts | Persistence | Tables on durable storage |
| Phone numbers must be valid Indian mobiles | Structure | CHECK (phone ~ '^[6-9][0-9]{9}$') |
| Reception, doctors and SMS service use the same data | Sharing | One database; roles per consumer |
| Two receptionists must not book the same slot | Concurrency | UNIQUE (doctor_id, slot_start) |
| Confirmed bookings must never disappear | Durability | WAL, sync commit, sync replica, backups |
CREATE TABLE appointment (
appointment_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
doctor_id BIGINT NOT NULL REFERENCES doctor,
patient_id BIGINT NOT NULL REFERENCES patient,
slot_start TIMESTAMPTZ NOT NULL,
status VARCHAR(12) NOT NULL CHECK (status IN ('BOOKED','CANCELLED','DONE')),
CONSTRAINT one_booking_per_slot UNIQUE (doctor_id, slot_start)
);What it shows. Each requirement maps to one of the reasons databases exist. "Appointments must survive restarts" is persistence; "phone numbers must be valid" is structure, enforced with a CHECK constraint so no client can insert a bad number; "three systems use the same data" is sharing, handled with one database and a role per consumer; "two receptionists must not book the same slot" is concurrency, made atomic with a UNIQUE constraint on doctor and slot; and "confirmed bookings must never disappear" is durability, provided by the write-ahead log, synchronous commit, a synchronous replica and backups.
Takeaway. Translate requirements into database mechanisms explicitly, one row per rule, and put each rule where it holds for every writer. Rules enforced only in one application's code are broken the first time another system or a manual fix writes to the same tables.
6.4 Worked example — proving concurrency safety with a test
Scenario. The booking service passed every unit test, yet in production two patients occasionally received the same 10:30 slot with the same doctor. The team believed their "check availability, then insert" logic was correct, because it never failed when tested one request at a time.
@Testcontainers
@SpringBootTest
class SlotBookingConcurrencyTest {
@Container @ServiceConnection
static PostgreSQLContainer<?> db = new PostgreSQLContainer<>("postgres:17");
@Autowired BookingService booking;
@Test
void onlyOneOfTwentyConcurrentBookingsSucceeds() throws Exception {
var results = new ConcurrentLinkedQueue<Boolean>();
try (var exec = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 20; i++) {
exec.submit(() ->
results.add(booking.tryBook(DOCTOR, SLOT, newPatient())));
}
}
assertThat(results).containsOnlyOnce(true);
}
}| What the test shows | Why it matters |
|---|---|
| Exactly one booking succeeds | The UNIQUE constraint enforces the rule under concurrency |
| No application locks needed | Works across many app instances |
What it shows. The test starts a real PostgreSQL in a container and fires twenty booking attempts for the same doctor and slot at the same moment, using virtual threads. It then asserts that exactly one succeeded. Against the old check-then-insert code, several attempts pass the availability check before any of them inserts, so the test fails. Once the UNIQUE (doctor_id, slot_start) constraint is added and the service treats a unique-violation as "slot taken", exactly one booking succeeds every time, because the database serialises the conflicting inserts.
Takeaway. Concurrency bugs do not appear in sequential tests. Prove concurrency guarantees with a concurrent test against the real database engine, not an in-memory substitute, and let constraints, not application checks, enforce the rule.
6.5 Worked example — choosing durability settings per data class
Scenario. A retail platform runs one PostgreSQL cluster for very different data: payments, user profiles, session logs and click streams. Applying the strictest durability to everything made write latency high for logging traffic, while relaxing it globally would have risked losing payments after a crash.
| Data | Loss tolerance | Settings |
|---|---|---|
| Payments, orders | None | synchronous_commit=on + synchronous standby, PITR backups |
| User profiles | None | Same as above |
| Session/activity logs | Seconds acceptable | SET LOCAL synchronous_commit = off in those transactions |
| Metrics/click streams | Minutes acceptable | Separate database or unlogged/partitioned tables |
What it shows. The team classified data by how much loss the business can tolerate and chose settings per class. Payments, orders and profiles keep synchronous_commit on with a synchronous standby and point-in-time recovery, so a committed transaction is never lost. Session and activity logs, where losing a few seconds is acceptable, use SET LOCAL synchronous_commit = off inside those transactions only, which removes the wait for the disk flush. Metrics and click streams, where minutes of loss are tolerable, move to a separate database or to unlogged or partitioned tables designed for high write volume.
Takeaway. Durability is a business decision per data class, not one global switch. Make the loss tolerance explicit with data owners, apply weaker settings only to the transactions that can afford them, and keep money and orders on the strongest guarantees.
7Common Problems & Mistakes
- Using files or caches as a system of record for shared business data.
- Application-level uniqueness checks without database constraints.
- Disabling durability globally for performance.
- Sharing writable tables across services.
- Assuming the ORM provides concurrency safety without transactions and constraints.
- No tested backups — durability ends at the last restorable backup.
8Performance & Production Considerations
| Aspect | Note |
|---|---|
| Commit latency | Dominated by WAL fsync (~sub-ms on NVMe, more on network disks) |
| Connection count | Each client needs connections; use pools (1.3) |
| Concurrency hotspots | Rows updated by many transactions (counters) serialise writes |
| Backups | Test restores regularly; measure RTO |
| Replication lag | Read replicas are eventually consistent |
9Architecture & Design Considerations
- Treat the database as the system of record and use its guarantees deliberately.
- Enforce invariants in the schema (constraints) and translate violations in the application.
- Choose durability levels per data class explicitly.
- Keep one owner per database in service architectures; share via APIs, events or read replicas.
10Interview Questions & Answers
Why use a database instead of files?
Files store bytes; a database manages data. With files, every application must invent its own format, parse it, handle partial writes after a crash, and coordinate concurrent writers, usually badly. A database provides structured storage with a schema and constraints, safe concurrent access through locking or MVCC, transactions that apply several changes atomically, crash recovery through a write-ahead log, and a declarative query language so you ask for data instead of writing loops. Many applications and users can share the same data safely. Files still suit large immutable objects such as images or exports, usually stored next to a database rather than instead of one.
What does durability mean?
Durability is the D in ACID: once the database reports that a transaction has committed, its changes must survive any later crash, power failure or restart. If a customer sees "order confirmed", that order must still exist after the server reboots. Durability is not the same as availability or backup: a durable database can still be unreachable during an outage, and it does not protect against someone deleting data on purpose. In practice, durability also depends on the hardware honouring flushes and, for protection against losing a whole server, on replication to another machine before commit is acknowledged.
How does a database guarantee durability?
Before acknowledging a commit, the database writes a description of the changes to its write-ahead log and forces that log to durable storage with an fsync. The actual table and index pages can be written later, because after a crash the database replays the log from the last checkpoint and reapplies every committed change, while discarding uncommitted ones. PostgreSQL calls this the WAL, Oracle the redo log. Synchronous replication extends durability beyond one machine by also waiting for a standby to receive the log. Settings such as synchronous_commit control how strictly this happens, trading latency against possible data loss.
Why is an application-level "check then insert" unsafe for uniqueness?
Check-then-insert runs as two separate steps: first a SELECT to see whether the value exists, then an INSERT if it does not. When two requests arrive at nearly the same moment, both SELECTs can run before either INSERT, both see no existing row, and both insert, creating a duplicate. Wrapping the steps in a transaction does not help at the default READ COMMITTED isolation level. A UNIQUE constraint makes the rule atomic inside the database: the second insert fails with a unique-violation error, which the application can turn into a clear message such as "this slot is already booked".
What is MVCC and why does it help concurrency?
Multi-version concurrency control keeps several versions of a row instead of overwriting it in place. When a transaction updates a row, the database creates a new version and keeps the old one until no running transaction needs it. Each transaction reads from a consistent snapshot, seeing only versions committed before its snapshot was taken. As a result, readers never wait for writers and writers never wait for readers; only two writers changing the same row must wait for each other. PostgreSQL, Oracle and MySQL's InnoDB all use MVCC. The cost is cleanup work, such as PostgreSQL's VACUUM, to remove old row versions.
When might you relax durability?
Relax durability only for data whose loss the business has explicitly accepted, such as click streams, metrics, activity logs or cache-like tables that can be rebuilt. In PostgreSQL, SET LOCAL synchronous_commit = off inside those transactions lets commits return before the log is flushed, so a crash can lose the last fraction of a second of such writes but never corrupts the database. Unlogged tables go further and lose all their data after a crash. Never relax durability for money, orders or anything with legal or customer impact, and document each relaxation with its owner's approval.
Why is sharing a database between services problematic?
When several services read and write the same tables, the database schema becomes a shared contract that nobody owns. Any change, such as renaming a column, can break another service, so deployments must be coordinated and the services are no longer independent. Ownership blurs: it becomes unclear which service enforces which rules, and data can be changed by code that does not understand them. Performance couples too, because one service's heavy query can slow all the others. The usual solution is a database per service, with data shared through APIs or events, and read-only replicas or warehouses for reporting.
How do you decide which guarantees the database should enforce?
Put in the schema every invariant that must hold no matter which application or person writes the data: primary and unique keys, foreign keys, NOT NULL, CHECK constraints for value domains, and exclusion constraints such as non-overlapping bookings. These are cheap, atomic and impossible to bypass. Keep in application code the rules that depend on workflow, user context or several aggregates, such as approval processes, pricing policies or rules that involve calls to other services. A useful test is: would corrupted data be possible if a new client or a manual SQL fix ignored this rule? If yes, the database should enforce it.