Data Integrity
In a distributed system, data is constantly moving between the client, the server, and the database. Data integrity is the architectural guarantee that this information remains accurate, consistent, and reliable throughout its entire lifecycle. If your application state becomes corrupted, the entire system loses its value. Professional engineers never trust the frontend to maintain data purity; they enforce strict rules at the deepest layers of the backend.
The ACID properties
Relational databases rely on four core principles to guarantee that transactions are processed reliably, even during system crashes or power failures.
- Atomicity: A transaction is treated as a single, indivisible logical unit of work. If a user transfers money, the deduction and the deposit must both succeed, or both fail completely.
- Consistency: Data must always adhere to defined rules and constraints. A transaction cannot leave the database in an invalid state, such as an account balance dropping below zero if a positive constraint exists.
- Isolation: Concurrent transactions execute independently without interfering with each other. If two users buy the last ticket simultaneously, the database must serialize the requests to prevent double-selling.
- Durability: Once a transaction is committed, it remains in the system permanently. Even if the server immediately loses power, the data is safe on the disk.
Architectural Trade-off: Enforcing strict ACID properties requires database locking, which reduces the overall throughput of your system. NoSQL databases often relax these rules in favor of “eventual consistency” to achieve massive horizontal scale.
Enforcing constraints at the database level
The database is the final line of defense against bad data. You must encode your business rules directly into the schema to prevent corrupted records from ever being saved.
- Primary and foreign keys: These prevent orphaned records and ensure that relationships between tables remain strictly intact.
- Unique constraints: These guarantee that no two rows share the same value for a specific column, which is essential for email addresses or usernames.
- Check constraints: These enforce specific mathematical or logical conditions on a column, such as ensuring a discount percentage never exceeds one hundred.
Preventing race conditions with locking
In a highly concurrent full-stack application, multiple requests often try to modify the exact same piece of data at the exact same millisecond.
- Optimistic locking: The system assumes conflicts are rare. It reads a record, notes a version number, and only allows an update if the version number has not changed since the read occurred. This is highly efficient for read-heavy applications.
- Pessimistic locking: The system assumes conflicts are highly likely. It locks the database row the moment it is read, forcing all other requests to wait in a queue until the first transaction finishes. This prevents conflicts entirely but slows down the application significantly.
/**
* Example of Optimistic Locking in a service function.
* It uses a version column to prevent two users from
* overwriting each other's updates simultaneously.
*/
const updateDocument = async (documentId, newContent, currentVersion) => {
// The database will only update the row if the version matches exactly
const result = await db.documents.update({
where: {
id: documentId,
version: currentVersion
},
data: {
content: newContent,
version: currentVersion + 1 // Increment the version for the next update
}
});
if (!result) {
throw new Error("Conflict: The document was modified by another user.");
}
return result;
};
Validating data across the stack
While the database is the final authority, relying solely on database errors creates a terrible user experience. A professional architecture validates data at every single layer of the stack.
- Client-side validation: The frontend immediately blocks invalid inputs, such as malformed email addresses, providing instant feedback to the user without wasting network resources.
- API payload validation: The server intercepts the incoming request and verifies the data shape using schema libraries, rejecting bad payloads before they reach the core business logic.
- Database enforcement: The schema catches edge cases and race conditions that bypass the application layer, throwing a hard error to prevent permanent corruption.
Learning Point: Never expose raw database constraint errors directly to the client. Always catch these errors in the server logic and translate them into human-readable messages, such as “This email is already in use,” instead of “Unique constraint violation on users_email_key.”
By building strict constraints into the database and managing concurrent transactions carefully, you ensure that the application’s source of truth remains pristine. This allows the frontend and backend to operate with absolute confidence in the data they process.
Popular Guides