Projektiranje baza podataka za otpornost: Arhitektura predvidljivosti pod opterećenjem
Most database failures do not begin with a dramatic outage. They begin as small deviations from the expected path: a slow query during a traffic spike, a locked row held a little too long, a retry that duplicates work, or a replica that is seconds behind when the application assumes it is current.
Resilient database design is the work of making those deviations predictable. The goal is not to promise that nothing will fail. Networks break, processes restart, disks fill, and dependencies become slow. The goal is to ensure that failure stays bounded, understandable, and recoverable.
Start with the promises your data must keep
A schema is more than tables and indexes. It encodes business promises. Before choosing a constraint or transaction boundary, identify which statements must always be true.
For an order system, examples might include: an order belongs to one customer; an item cannot be fulfilled twice; a payment reference cannot be processed more than once; and inventory cannot become negative if the business does not allow overselling. These are invariants, and the database should enforce the ones it can enforce reliably.
- Use primary keys to establish stable identity.
- Use foreign keys where the relationship must not be broken.
- Use unique constraints for business identifiers and idempotency keys.
- Use check constraints where supported and appropriate for local value rules.
- Keep application validation, but do not treat it as the only line of defense.
Application code can validate an input before writing it, but concurrent requests can still pass the same validation at the same time. A unique constraint resolves that race at the point where it matters: the shared source of truth.
Model for change without abandoning clarity
Overly rigid schemas make change expensive. Overly loose schemas make correctness expensive. Resilience comes from choosing structure deliberately.
Normalize data when it prevents contradictory copies of important facts. A customer address stored in several unrelated tables may look convenient until one copy changes and the others do not. At the same time, do not force every read path to reconstruct a complex domain from dozens of joins. Derived read models, summary tables, and cached projections can be sensible when they are explicitly treated as derived data with a refresh and recovery strategy.
Flexible fields such as JSON can be useful for metadata that truly varies. They are less useful as an escape hatch for core fields that need validation, indexing, joining, reporting, or lifecycle rules. If a field becomes central to application behavior, promoting it to a well-defined column is often the more maintainable decision.
Make writes safe to repeat
In distributed systems, a client may not know whether a request succeeded. The database may commit a transaction just before the connection drops. A queue consumer may finish its work and crash before acknowledging the message. Retrying is necessary, but retries must not create a second payment, email, shipment, or record.
Idempotency turns “try again” into a safe operation. Accept a client-supplied idempotency key for a meaningful operation, store it with a uniqueness guarantee, and return the original result when the same key is submitted again. The key must represent the operation, not merely the HTTP request.
CREATE TABLE payment_requests (
id BIGINT PRIMARY KEY,
idempotency_key VARCHAR(255) NOT NULL,
customer_id BIGINT NOT NULL,
amount DECIMAL(12,2) NOT NULL,
status VARCHAR(32) NOT NULL,
UNIQUE (idempotency_key)
);
The uniqueness constraint is essential. A “check first, then insert” approach alone is vulnerable to concurrent requests. In PHP, catch the database’s duplicate-key error, load the existing request, and return its known outcome. Do not broadly retry every exception; retries are appropriate for transient failures, not invalid input or violated business rules.
Keep transactions short and purposeful
Transactions protect consistency, but long transactions also hold locks, consume resources, and amplify contention. A good transaction does the minimum work needed to move data from one valid state to another.
Avoid making remote API calls, rendering documents, sending emails, or waiting for user input inside a database transaction. Persist the state change first, then hand off external work through a durable mechanism.
Use an outbox for reliable side effects
The transactional outbox pattern solves a common gap: a service updates its database but fails before publishing the corresponding event. In the same transaction that creates an order, insert an outbox row describing the event. A separate worker reads pending rows, delivers them, and marks them complete. Consumers should still be idempotent, because delivery can occur more than once.
This pattern makes the boundary explicit. The database transaction guarantees the business change and the intent to publish; it does not pretend to make the database and a message broker one atomic system.
Design queries for the uncomfortable day
A query that is fast with a small dataset may become the dominant source of load later. The answer is not to index every column. Every index has a write cost, storage cost, and maintenance cost. Instead, index the access paths the application actually needs.
Inspect query plans for high-volume and latency-sensitive operations. Match composite index order to the filtering and ordering pattern. Select only required columns. Avoid unbounded queries in request paths, and use keyset pagination for large, ordered result sets when stable traversal matters.
SELECT id, created_at, status
FROM orders
WHERE customer_id = :customer_id
AND id < :last_seen_id
ORDER BY id DESC
LIMIT 50;
An index beginning with customer_id and then id may support this access pattern, subject to the database engine and the rest of the query. Verify rather than assume: the execution plan is the evidence.
Plan for contention, lag, and degraded dependencies
Concurrency failures deserve first-class treatment. Two workers may attempt to reserve the last item. Two administrators may edit the same record. A background job may race a customer request. Choose the correct strategy for each case: optimistic concurrency with a version column, carefully scoped row locking, or a business workflow that records a pending state.
Read replicas introduce another tradeoff. They can reduce read pressure, but replica lag means a just-written value may not immediately appear on a replica. Route read-after-write flows to the primary when freshness is required, or design the interface to acknowledge that a result is still processing. Consistency is a product behavior as much as a storage setting.
Set timeouts deliberately across the stack: connection acquisition, query execution, HTTP clients, workers, and load balancers. Without timeouts, a slow dependency can turn into an exhausted connection pool and then an application-wide outage. With sensible limits, the system can fail a smaller number of requests quickly and recover capacity.
Practice recovery before you need it
Backups are not a recovery plan until restoration has been tested. Know which data must be restored, how long recovery can take, where credentials and encryption keys live, and how application writes are controlled during recovery. Test the procedure in an isolated environment using realistic data size and dependencies.
Operational visibility completes the design. Monitor database saturation, connection usage, slow queries, lock waits, replication health, error rates, and backup success. More importantly, make alerts actionable. An alert that says “database is slow” is less useful than one that identifies sustained pool exhaustion, a growing queue, or a failing backup job.
Predictability is the real performance feature
Resilient databases are rarely defined by one clever schema trick or a powerful server setting. They are built from modest, reinforcing choices: clear invariants, constraints that enforce them, repeatable writes, short transactions, measured queries, bounded retries, and rehearsed recovery.
That discipline gives a system its most valuable quality under strain: it behaves in ways the team can explain. When the next incident arrives, predictability turns panic into a sequence of decisions—and that is what keeps a database dependable long after the first successful deployment.