What you'll learn
Persistence code translates between domain operations and durable relational state. Parameterized queries, transactions, migrations, and small repository boundaries protect correctness.
By the end of this lesson, you'll be able to:
- Model relational data and constraints
- Use parameterized queries
- Define transaction and repository boundaries
Core mental model
Use this decision table as a compact reference. Focus on what each tool means and when it earns its place in production code.
| Concept | What it means | Decision rule |
|---|---|---|
| Constraint | Database-enforced invariant | Enforce uniqueness and relationships at the durable boundary |
| Transaction | Atomic group of reads and writes | Use when partial completion would violate an invariant |
| Repository | Domain-focused persistence interface | Hide query mechanics, not domain decisions |
Professional workflow
Build the behavior in small, observable steps. Each step should leave something you can inspect or test.
- Describe the persistence transaction boundary: inputs, outputs, state, timing, and expected failures.
- Implement the smallest correct path with names that expose intent.
- Add edge cases and failure handling before introducing abstractions.
- Verify behavior with realistic data and one deliberately adversarial example.
- Refactor only after the observable behavior is protected.
Make behavior observable
Guided code lab
Parameterize user input
The SQL structure stays separate from values, preventing injection.
export async function findCourseBySlug(db, slug) {
const result = await db.query(
"SELECT id, slug, title FROM courses WHERE slug = $1",
[slug],
);
return result.rows[0] ?? null;
}Protect a multi-write invariant
Commit only after both updates succeed; otherwise rollback.
await db.query("BEGIN");
try {
await db.query("INSERT INTO enrollments(user_id, course_id) VALUES ($1, $2)", [userId, courseId]);
await db.query("UPDATE courses SET seats = seats - 1 WHERE id = $1 AND seats > 0", [courseId]);
await db.query("COMMIT");
} catch (error) {
await db.query("ROLLBACK");
throw error;
}Production practice
Contract
Make the persistence transaction boundary explicit with validated inputs, structured outputs, owned resources, and stable failures.
Verification
Exercise normal work, invalid input, dependency failure, concurrency, and graceful cleanup in automated tests.
Operations
Use structured logs, health signals, timeouts, and configuration that can change without editing source code.
Common failure mode
Independent workshop
Design persistence for courses, learners, and enrollments.
Your finished workshop must include:
- Schema with keys and constraints
- Parameterized repository methods
- A transaction plus a rollback integration test
Definition of done
Recap & quick check
Key takeaways
- Constraints protect durable truth
- Parameters separate code and data
- Transactions make work atomic
- Repositories expose domain-oriented persistence
Quick check
1. What prevents SQL injection most directly?
2. When is a transaction needed?
3. What should migrations be?
Keep the workshop. Later modules deliberately build on these decisions, so each exercise can become part of your final portfolio architecture.