Database Design for Backend Systems
Explore relational modelling, keys, relationships, normalisation, indexes, transactions, connection pools, query planning, pagination, replication and backups.
Concept Overview
The database is a major persistent component of a backend system. For the Task Management API, PostgreSQL is the source of truth for users, projects, memberships, tasks and comments. Database design determines how these entities relate, how integrity is enforced, how queries are served and how persisted data is recovered. Good database design balances correctness and query performance with write cost and operational complexity.
1. Model entities and relationships
The supplied schema notes identify users, projects, project_members, tasks and comments. A user can own multiple projects; a project contains many tasks; a project can have many users and users can belong to many projects; a task can have many comments. The many-to-many relationship between users and projects is represented by a junction table such as project_members. This table can also store membership-specific attributes such as role and creation time.
User 1 -------- N Projects
Project 1 ----- N Tasks
Project N ----- N Users (project_members)
Task 1 -------- N Comments2. Primary keys and foreign keys protect identity and relationships
A primary key uniquely identifies a row, using an integer or UUID depending on the design. A foreign key references a row in another table and lets the database enforce relational integrity. For example, projects.owner_id can reference users.id and tasks.project_id can reference projects.id. Database constraints are valuable because they protect data integrity even when writes originate from different application code paths.
3. Normalisation reduces duplication
Normalisation aims to avoid storing the same facts repeatedly. If every task row copied project_name, project_owner_name and project_description, updates could leave inconsistent copies. Storing project information in the projects table and referencing it from tasks through project_id preserves one authoritative project record. Normalisation is not an end in itself; denormalisation may be considered for measured read patterns, but it creates a consistency responsibility.
4. Indexes accelerate selected reads but add write cost
An index helps the database locate matching rows efficiently. A query that commonly filters tasks by project_id may benefit from an index on that column. If a frequent query filters by project_id and status together, a composite index may help. Indexes consume disk space and require maintenance; they can slow inserts, updates and deletes because index structures must also be maintained. Do not create an index for every column. Inspect actual query patterns and query plans before choosing indexes.
CREATE INDEX idx_tasks_project_id
ON tasks(project_id);
CREATE INDEX idx_tasks_project_status
ON tasks(project_id, status);5. Transactions protect multi-record operations
A transaction groups related operations into an atomic unit. Creating a project and establishing its owner membership should succeed together or fail together. If one insert succeeds but the other fails, the database should roll back the entire operation. Transactions are also relevant to task assignment and related state changes. Choose transaction boundaries around one logical business operation, and avoid holding transactions open while waiting on unrelated remote services.
BEGIN;
INSERT INTO projects (...);
INSERT INTO project_members (...);
COMMIT;
-- If an operation fails: ROLLBACK;6. Connection pooling and query optimisation
Opening a database connection for every request is expensive. A connection pool such as HikariCP reuses a bounded number of connections. Pool sizing needs to account for all application instances and the database's total connection budget. Query optimisation also includes selecting only the columns needed, avoiding unbounded reads, and addressing N+1 query patterns with suitable fetch joins, entity graphs, explicit joins, batch fetching or projections depending on the ORM and use case.
7. Pagination controls response size
A task endpoint should not return hundreds of thousands of rows in one response. Offset pagination is straightforward: LIMIT and OFFSET select a bounded page. Very large offsets can become expensive because the database may need to walk past many rows. Keyset or cursor pagination uses the last seen ordering key to continue from a position and can be more efficient for large sequential result sets. Stable ordering is important for both approaches.
-- Offset pagination
SELECT id, title, status
FROM tasks
ORDER BY id
LIMIT 50 OFFSET 1000;
-- Keyset pagination
SELECT id, title, status
FROM tasks
WHERE id > 1000
ORDER BY id
LIMIT 50;8. Replication, backups and recovery
A primary database handles writes while read replicas can serve selected read traffic. Replication may introduce lag, so a read immediately after a write may not be visible on a replica. Backups and point-in-time recovery are important, but a backup strategy is incomplete until restoration is tested. Recovery procedures should define acceptable data loss and recovery time for the system's requirements.
Key Takeaways
- ✓Model relationships explicitly and use constraints to protect integrity.
- ✓Indexes are workload-specific and impose write and storage costs.
- ✓Use transactions for related changes that must commit atomically.
- ✓Bound result sets, choose pagination based on data size, and test recovery procedures.