Razvoj

System Architecture: Why Your Database Design is the Real Bottleneck

Arhitektura sustava: Zašto je dizajn vaše baze podataka pravo usko grlo

Most performance problems do not begin with a slow PHP loop, an inefficient Docker image, or a missing cache header. They begin earlier: with a database model that makes ordinary work expensive.

When a system is small, weak data design can look harmless. A single table grows wide, a few fields contain comma-separated values, and application code fills the gaps. Then the product gains users, integrations, reporting requirements, and concurrent writes. Suddenly, every API endpoint carries hidden complexity because the database cannot express the business clearly or retrieve it efficiently.

Your database is not merely storage behind the application. It is the part of the architecture that decides which questions are cheap, which updates are safe, and how much complexity leaks into everything else.

Schema decisions become application behavior

Consider an API that creates orders. A naïve implementation may store customer details, line items, delivery information, and payment state in one large table. It can work for a basic checkout flow, but it quickly creates ambiguity. What happens when an order has multiple items? Can a customer update their address without rewriting historical order data? How do you calculate revenue by product without parsing serialized values?

A more deliberate model separates concepts that change independently:

  • Customers represent the current customer relationship.
  • Orders represent a point-in-time commercial transaction.
  • Order items represent the products, quantities, and prices captured for that transaction.
  • Addresses may be stored as snapshots on an order when historical accuracy matters.

This is not normalization for its own sake. It makes the rules visible. A database constraint can prevent an order item from referencing a non-existent order. A transaction can create an order and its items atomically. A query can aggregate product sales without asking PHP to decode and reconstruct data after the fact.

The opposite design often shifts correctness into scattered service methods. One endpoint validates a relationship; another forgets. A background worker updates a status differently from the web application. Over time, the real data model becomes an undocumented collection of assumptions.

Model the invariants before optimizing queries

Indexes and query tuning matter, but they cannot reliably repair a model that does not define its own rules. Start by identifying invariants: conditions that must always be true regardless of whether data arrives through a REST endpoint, a CLI command, a queue worker, or an administrative interface.

Examples include:

  • An email address is unique within the scope where it is used.
  • An invoice belongs to exactly one account.
  • An order item quantity is positive.
  • A payment cannot be captured twice for the same provider reference.

Where the database can enforce an invariant, let it. Use primary keys, foreign keys, unique constraints, appropriate nullability, and checks where supported by the chosen database. Application validation still matters because it produces useful user-facing errors, but it should not be the final line of defense for data integrity.

This is especially important in distributed backend systems. Retries are normal. A client can resend a request after a timeout even though the original request succeeded. A worker can be delivered the same message again. If “create payment” is only protected by an application-level lookup followed by an insert, two concurrent requests can both pass the lookup. A unique constraint on the provider reference gives the system a durable answer.

Make retries safe by design

For operations with external side effects, define an idempotency strategy. Store an idempotency key with a uniqueness rule appropriate to the endpoint or account. If the same request is received again, return the recorded result or safely recognize that the operation is already in progress.

The implementation details vary, but the architectural principle does not: correctness must survive concurrent requests, process restarts, and repeated delivery. A successful test using one browser tab says little about those failure paths.

Indexes are promises about access patterns

An index is not a decoration added after production becomes slow. It is a promise that a particular lookup, sort, or join is important enough to support efficiently.

Suppose an endpoint lists recent orders for one customer:

SELECT id, status, total, created_at
FROM orders
WHERE customer_id = ?
ORDER BY created_at DESC
LIMIT 50;

A composite index beginning with customer_id and then created_at aligns with that access pattern. Indexing only created_at may still leave the database examining many rows for other customers. Indexing every column, however, increases storage, slows writes, and makes maintenance more costly.

Choose indexes from real query shapes: filtering columns, join keys, ordering requirements, and expected selectivity. Then inspect execution plans in an environment with representative data. A query that feels instant on a development database with a few hundred rows can behave very differently when a broad filter meets millions of records.

Keep the database boundary honest

PHP frameworks make it easy to work with objects, relationships, and lazy loading. That convenience can hide expensive behavior. A page that loads a list of orders and then accesses a customer relation for each row may produce one query for the list and many more queries for the related records.

The fix is not to reject an ORM. The fix is to understand the SQL it produces and to shape data access intentionally. Eager-load when a known relationship is needed, select only the columns required, paginate large collections, and avoid using application memory as a substitute for a database query plan.

Likewise, resist turning the database into an unstructured document store merely because JSON columns are convenient. JSON is useful for genuinely variable attributes, external payload snapshots, or low-query metadata. It is a poor home for values that need foreign-key relationships, frequent filtering, reporting, or reliable uniqueness rules.

Architecture includes operational change

Database design is also about how safely the system evolves. A deployment that adds a non-null column, changes a large index, or rewrites a heavily used table can affect availability even when the PHP release itself is straightforward.

Prefer migrations that separate structural change from behavioral change. Add a nullable column first, deploy code that writes both representations if needed, backfill in controlled batches, validate the result, then enforce stricter constraints later. This approach creates room for rollback and reduces the risk of long-running locks or incompatible application versions.

Docker does not remove these concerns. Containers make application processes reproducible; they do not make schema changes transactional across a fleet of running instances. Treat migrations as a deliberate deployment step with clear ownership, observability, and a recovery plan.

The bottleneck you can design away

A good database design does not guarantee a fast or maintainable system. It does something more valuable: it gives the rest of the system stable ground. APIs gain predictable semantics, background jobs gain reliable state transitions, reports become queries instead of recovery projects, and performance work becomes focused rather than desperate.

Before optimizing the next endpoint, ask a simpler question: what facts does this system need to preserve, and what questions must it answer cheaply? The quality of those answers will shape your architecture long after today’s framework, container, and caching strategy have changed.

Portret autora bloga

Mihajlo

Ja sam Mihajlo — programer vođen znatiželjom, disciplinom i stalnom željom da stvorim nešto smisleno. Dijelim uvide, tutorijale i besplatne usluge kako bih pomogao drugima da pojednostave svoj rad i rastu u svijetu softvera i umjetne inteligencije koji se neprestano razvija.