Arhitektura sustava: Promišljanje baza podataka za nepokolebljivu izvedbu
A database rarely becomes the problem all at once. More often, a system slowly teaches itself bad habits: a convenient query becomes a shared dependency, a background job starts competing with customer requests, and every new feature adds one more round trip across the network. Then traffic rises, latency becomes unpredictable, and the database is blamed for faithfully executing an architecture that asks too much of it.
Unflappable performance is not about finding a magical database product or adding replicas after an incident. It comes from designing clear boundaries around data, understanding request paths, and making the expensive work visible before it becomes urgent.
Start with the work, not the database
The first architectural question is not “SQL or NoSQL?” It is “what work must happen synchronously for this user action to succeed?” A checkout confirmation, an authentication decision, and an inventory reservation may need strong consistency and an immediate answer. Sending an email, generating a report, rebuilding a search index, or calculating analytics usually does not.
When these responsibilities share the same request path and the same database resources, the slowest concern determines the experience for everyone. A resilient system separates critical transactional work from work that can be delayed, retried, or processed in batches.
For a PHP application, that might mean keeping a web request deliberately narrow: validate input, execute the required transaction, persist an event or job, and return a response. A worker can then process the nonessential follow-up work outside the customer’s request budget.
$orderId = $orderService->place($command);
$queue->dispatch(new SendOrderConfirmation($orderId));
return response()->json(['order_id' => $orderId], 201);
This pattern does not make complexity disappear. It moves complexity to a place where retries, observability, and controlled throughput are possible. That is a much better trade than making every browser request wait for unrelated work.
Protect the transactional core
A relational database is exceptionally good at enforcing rules: foreign keys, uniqueness, transactions, and constrained updates. Those strengths matter most when data represents a business commitment. Treat that part of the model as a transactional core, not as a universal storage engine for every read model, audit stream, cache, and reporting need.
Keep transactions short. Do not call an external API while holding a database transaction open. Avoid loading a large object graph merely to update one field. Select the columns required for the operation, perform the smallest valid change, and commit promptly.
Concurrency deserves equal attention. A read-modify-write sequence can look correct in a quiet development environment and fail under load. If stock must never become negative, express that rule in the update or use an appropriate locking strategy. The exact approach depends on the database and business rule, but the architecture must acknowledge that concurrent requests exist.
UPDATE inventory
SET available = available - :quantity
WHERE sku = :sku
AND available >= :quantity;
The application should check whether one row was updated. If not, it can return an out-of-stock result without first reading a value that may change before the write occurs.
Design reads as carefully as writes
Many performance problems are not caused by slow writes. They are caused by reads that accidentally multiply. An API endpoint that returns a list of orders and then loads the customer and line items one record at a time can turn a modest page into dozens or hundreds of queries.
Measure the query count and shape for important endpoints. Use eager loading where it fits, but do not treat eager loading as a blanket solution. It can replace an N+1 problem with an oversized join or a huge result set. For a list page, a purpose-built query or projection is often clearer than reusing a rich domain model.
- Paginate every potentially unbounded collection.
- Index columns used for selective filters, joins, and deliberate sort orders.
- Inspect query plans before assuming an index helps.
- Return fields the client needs, rather than entire database records by default.
- Set explicit limits for administrative and reporting endpoints too.
An index is not free. It consumes storage and makes writes more expensive because each change may update index structures. The goal is not maximum indexing; it is indexes that support known, important access patterns.
Use caches as a performance layer, not a source of truth
Caching is valuable when it prevents repeated, expensive work. It is dangerous when it quietly becomes the only place an application can find correct data. The database should remain the authoritative source for transactional facts unless the architecture explicitly establishes another authority.
Cache stable or frequently requested derived data: configuration snapshots, catalog pages, rate-limit counters, or carefully scoped API responses. Give every cache entry an ownership model. Who invalidates it? What happens when invalidation fails? Is slightly stale data acceptable? If those questions have no answer, the cache may be hiding a correctness bug.
Cache keys should include the dimensions that affect the response, such as tenant, locale, permissions, and relevant filters. A fast cache response delivered to the wrong tenant is not a performance win.
Make failure a normal operating condition
Database connections can be exhausted. A replica can lag. A queue can back up. A deploy can introduce an inefficient query. Architecture becomes dependable when these conditions have bounded effects instead of cascading across the entire application.
Set connection pools and worker concurrency according to the database’s capacity, not according to how many application containers can be launched. More PHP workers can increase throughput until they create contention; beyond that point, they simply create a larger waiting room in front of the database.
For background work, use retries deliberately. Retry only failures likely to be temporary, use a finite retry policy, and make handlers idempotent where possible. A job that charges a payment, publishes a message, or creates a record must tolerate delivery more than once without repeating the business effect.
Deploy schema changes in safe stages
Schema migrations are architectural events, not housekeeping. A change that works on a small local database can create long locks or incompatible application states in production. Favor additive changes: add a nullable column, deploy code that can handle both shapes, backfill in controlled batches, then enforce tighter constraints after the old path is gone.
This approach also makes rollbacks less frightening. If an application version understands both the previous and new schema, deployment does not depend on a perfectly timed all-or-nothing switch.
Observe the system at its boundaries
Useful observability connects a slow request to its dependencies. Track request latency, error rates, database query duration, connection usage, queue depth, job failures, and the rate of cache misses. Correlate requests with logs or traces so a developer can distinguish an expensive query from a downstream timeout or a saturated worker pool.
Metrics should guide investigation, not replace judgment. A low average latency can conceal a painful tail of slow requests. A database with low CPU can still be constrained by locks, inefficient plans, connection pressure, or storage latency. Ask what resource is actually waiting and why.
Build for calm
The best database architecture makes ordinary change boring. New endpoints have predictable query patterns. Background work has a controlled path. Data rules live close to the data. Deployments tolerate mixed application versions. And when load increases, the team knows which limit to measure before reaching for more infrastructure.
Unflappable performance is not the absence of failure. It is the ability to contain failure, understand it quickly, and keep the important path small enough to remain reliable. Design that path with care, and the database stops being a mysterious bottleneck. It becomes what it should be: a dependable part of a system that knows how to stay calm under pressure.