Razvoj

Beyond the AI Blueprint: Own Your Architecture's Enduring Value

Iza nacrta za umjetnu inteligenciju: Posjedujte trajnu vrijednost svoje arhitekture

AI can produce a plausible architecture in seconds: a service diagram, a Docker Compose file, a queue, a cache, and a handful of API endpoints. That speed is useful. It is also dangerous when it creates the impression that architecture is mainly the act of assembling familiar components.

The enduring value of an architecture is not its diagram. It is the set of decisions that continues to make change safe after the diagram has become outdated. It lives in boundaries, ownership, failure behavior, data rules, operational habits, and the ability of a team to explain why a system works the way it does.

Use AI to accelerate exploration. Do not outsource the judgment that turns a generated blueprint into a system worth operating.

Architecture is a sequence of commitments

Every architectural choice commits the future team to certain costs. A modular monolith commits you to maintaining clear internal boundaries. Microservices commit you to distributed tracing, network failures, versioned contracts, and independent deployment discipline. Event-driven workflows commit you to eventual consistency, duplicate delivery, and recovery procedures.

None of these approaches is inherently modern or outdated. The useful question is simpler: what problem does this commitment solve, and can the organization carry its cost?

For a PHP backend, a straightforward application with a relational database is often the strongest starting point. A well-structured monolith can support separate modules for billing, identity, catalogue, and reporting without requiring separate processes. Its transaction boundaries are easier to understand, local development is simpler, and a cross-module change can remain a single deployment.

Splitting a module into a service becomes compelling when it needs genuinely independent scaling, reliability isolation, security controls, or a separate release cadence. “It might grow” is rarely enough. Most systems grow more safely when their internal design is ready for separation before their infrastructure is separated.

Make boundaries visible in code and data

A boundary is real only when it changes what code is allowed to know. Putting directories named Orders and Payments beside each other is a helpful start, but not a guarantee. If every controller can query every table and import every class, the application still has one large, implicit dependency graph.

Prefer explicit interfaces at the edges of a module. An order module may ask a payment module to authorize a payment through an application-level interface. It should not reach into payment persistence tables and reconstruct payment state for itself.

This principle is especially important around databases. Shared tables are convenient until they become a permanent, undocumented API. If multiple parts of a system need the same data, decide whether they need the same source of truth, a read model, or an event that communicates a state change. Those options have different consistency and maintenance trade-offs.

Protect database changes like public contracts

Schema migrations are deployments of durable behavior. A column rename may break an old worker, a reporting query, or a background process that has not yet been redeployed. Prefer additive changes first: add a new column, write both representations where needed, migrate existing data, switch readers, and remove the old field only when every consumer has moved.

The same discipline applies to APIs. A response field that clients depend on is a contract, even if the endpoint is internal. Add fields rather than changing their meaning. Validate input deliberately. Return errors in a consistent shape. Document whether an operation is idempotent, particularly for payment, provisioning, and webhook endpoints.

public function createOrder(CreateOrderRequest $request): Response
{
    $idempotencyKey = $request->header('Idempotency-Key');

    if ($idempotencyKey === null) {
        return $this->json(['error' => 'Idempotency-Key is required'], 400);
    }

    $result = $this->orderService->createOnce(
        $idempotencyKey,
        $request->validated()
    );

    return $this->json($result, 201);
}

The controller is not the whole solution. The service must atomically associate the key with the resulting operation, handle retries, and define what happens when a repeated request carries different input. Architecture means following the behavior all the way through, not stopping at an attractive endpoint signature.

Design for ordinary failure

Networks time out. Containers restart. Queue messages arrive more than once. A dependency can accept a request but fail before its response reaches you. These are routine conditions, not exotic edge cases.

Retries should be scoped and intentional. Retrying a safe read after a transient connection failure may be reasonable. Retrying a non-idempotent write without a durable idempotency strategy can create duplicate orders or emails. Background jobs need clear retry limits, useful logs, and a place for exhausted work to be inspected rather than silently discarded.

Docker improves repeatability, but it does not erase operational design. A container image should contain the application and its runtime dependencies; configuration should enter through the environment or deployment platform. Health checks should reflect whether the application can serve its required purpose, not merely whether a process exists. A PHP-FPM process can be alive while its database credentials are invalid.

Performance begins with observation

Performance work is often framed as a choice of cache, database, or language feature. The first question should be: where is time actually going? Measure request duration, query counts, slow queries, error rates, queue delay, and resource pressure before declaring a bottleneck.

A familiar example is an endpoint that loads a list of orders and then loads customer data one row at a time. The code may look harmless in development but create many database queries under real traffic. The correct remedy might be a join, eager loading, a batch lookup, or a purpose-built read query. It is not automatically “add Redis.”

Caching is valuable when its invalidation rule is explicit. Name the owner of the cache entry, the event that makes it stale, its acceptable age, and the fallback if the cache is unavailable. A cache with no answer to those questions is not a performance feature; it is a future correctness incident.

Keep the reasoning close to the system

Good architecture is legible. A new engineer should be able to find how a request enters the system, where authorization occurs, which transaction writes the record, what emits an event, and how an asynchronous failure is recovered.

  • Write concise decision records for consequential choices and their rejected alternatives.
  • Keep runbooks for recurring operational actions, including rollback and recovery.
  • Test critical contracts at boundaries: HTTP APIs, database migrations, queues, and external integrations.
  • Review changes for ownership and failure modes, not only style and happy-path correctness.

AI can draft code, compare options, and expose patterns you may have missed. Its output is most valuable when it gives experienced judgment more material to evaluate. The architecture remains yours when you can explain its trade-offs, observe its behavior, change it safely, and recover when it fails.

That ownership is the part no generated blueprint can provide. It is also the part that keeps delivering value long after the first implementation is forgotten.

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.