Pravi sjever vašeg backenda: arhitektura na koju se AI može osloniti
Most backend failures do not begin with a dramatic outage. They begin with uncertainty: a controller that quietly owns business rules, an API response whose shape changes by accident, a database query added “just for now,” or a Docker setup that behaves differently from production.
AI can accelerate implementation, but it cannot reliably infer a system’s real rules from a codebase that has none. If architecture is vague, generated code tends to make the vagueness executable. The goal is not to build an elaborate enterprise diagram. It is to give the backend a clear true north: boundaries, contracts, and decisions that people and AI tools can follow without guessing.
Architecture is a set of useful constraints
Good architecture is often misunderstood as a folder structure or a preferred framework pattern. Those things can help, but the deeper value is constraint. Architecture answers questions that would otherwise be repeatedly debated during implementation.
Where does a rule belong? Which layer may access the database? What does an API promise to callers? Which failures are expected, and how are they represented? How does a background job remain safe when it runs twice?
When these answers are consistent, a developer can make a change with confidence. An AI assistant can also generate a smaller, safer patch because the acceptable path is visible. It does not need to choose among three competing approaches hidden in the repository.
Make the boundaries obvious
A pragmatic PHP backend does not need dozens of abstractions. It does need a clear separation between transport concerns, application workflows, domain rules, and infrastructure.
- HTTP controllers should translate requests into application input and application results into responses.
- Application services should coordinate a use case: authorize an action, load data, apply rules, persist a change, and trigger follow-up work.
- Domain objects or focused rule classes should hold rules that matter independently of HTTP, queues, or a database.
- Repositories and infrastructure adapters should isolate database access, external APIs, caches, mailers, and storage.
The names are less important than the direction. A controller should not contain a five-step pricing calculation. A repository should not decide whether a customer is eligible for a refund. A queue handler should not quietly become a second version of an HTTP endpoint.
For example, a request to cancel an order should remain readable at the application boundary:
final class CancelOrderService
{
public function cancel(OrderId $orderId, UserId $actor): void
{
$order = $this->orders->get($orderId);
$this->authorization->assertCanCancelOrder($actor, $order);
$order->cancel();
$this->orders->save($order);
$this->events->dispatch(new OrderCancelled($orderId));
}
}
The details can vary, but the important decisions are easy to locate. Authorization is explicit. The order owns its state transition. Persistence happens after the rule succeeds. The event is a deliberate consequence, not an incidental side effect of a database trigger.
Design APIs as contracts, not controller output
An API is a product surface. Its users may be a frontend, another service, a mobile application, or a partner you have never met. Returning whatever a model currently looks like is convenient until an internal refactor becomes a breaking change.
Define request and response shapes intentionally. Validate input at the boundary, use stable field names, and decide what an error response means. A useful error should let a caller distinguish invalid input, missing resources, forbidden actions, conflicts, and temporary failures.
{
"error": {
"code": "order_cannot_be_cancelled",
"message": "This order can no longer be cancelled."
}
}
The message helps a human. The code helps software. The same principle applies to pagination, filtering, timestamps, nullable fields, and versioning. Document the contract near the implementation or in an API specification, then test it. AI-generated endpoint code is far more dependable when it is constrained by examples and contract tests rather than a one-line prompt.
Let the database protect the truth
Application validation is necessary, but it is not enough. Concurrent requests, imports, scripts, and future services can bypass assumptions made in one code path. The database should enforce the invariants it can enforce.
- Use foreign keys for relationships that must exist.
- Use unique constraints for identities and deduplication keys.
- Use appropriate nullability rather than treating every column as optional.
- Use transactions when multiple writes must succeed or fail together.
- Add indexes for real query patterns, not merely for columns that seem important.
Consider a payment callback that may be delivered more than once. “Check whether it was processed” in PHP is not sufficient if two workers run at the same time. A unique constraint on the provider event identifier gives the system a final line of defense. The application can handle the resulting conflict as an already-processed event.
Database constraints are not hostility toward developers. They are executable documentation for facts the system cannot afford to forget.
Build for retries and partial failure
Distributed systems fail in ordinary ways. A network call times out after the remote service has already accepted a request. A worker crashes after writing to the database but before sending an email. A deployment interrupts a long-running job.
Handle these cases as design inputs. Queue work should be idempotent where possible: running the same job twice should reach the same correct state. External requests should use idempotency keys when the provider supports them. Retry policies should distinguish transient failures from invalid requests that will never succeed.
For changes that must both update local state and publish a message, consider an outbox pattern. Store the domain change and the pending event in one database transaction, then let a worker publish the event reliably. This avoids the fragile sequence of “save first, publish second” where a crash can leave the two systems inconsistent.
Keep Docker boring and environments close
Docker is most valuable when it removes environmental surprises. A local stack should make dependencies explicit: PHP version, extensions, web server, database, cache, queue worker, and configuration. It should not conceal production-only behavior behind untracked scripts or manual setup.
Keep configuration in environment variables or managed configuration, validate required values at startup, and provide safe defaults only for development. Run migrations as an explicit deployment step with a known rollback or recovery plan. Avoid treating container rebuilds as database strategy.
Operational clarity also improves AI-assisted work. A repository that has one documented way to start services, run tests, execute migrations, and inspect logs is easier for any contributor to change safely.
Optimize after you can observe
Performance work begins with a question, not a cache. Measure request latency, error rates, database query count, slow queries, queue depth, and resource consumption. Then follow the evidence.
In PHP applications, common improvements are often straightforward: eliminate N+1 queries, select only needed columns, paginate large result sets, add a justified index, move slow nonessential work to a queue, and cache data with a clear invalidation strategy. Each optimization has a maintenance cost. A cache without ownership or expiry rules is frequently a delayed correctness bug.
A reliable backend is legible
The best architecture is not the one with the most patterns. It is the one that makes the next correct decision easier than the next expedient mistake. Its rules are visible in code, database constraints, tests, API contracts, and operational documentation.
That legibility matters even more when AI joins the development process. Give it stable conventions, small interfaces, representative tests, and explicit failure behavior. Then it can help produce useful code inside the system’s guardrails instead of widening every crack in the design.
Your backend’s true north is not a diagram on a wall. It is the collection of decisions that remain clear when the code changes, the team grows, traffic rises, and implementation becomes faster than review. Build those decisions into the system, and reliability becomes a direction rather than a rescue operation.