ИТ развој

Stop Chasing Bugs, Start Architecting Robustness

Престанете да бркате грешки, почнете да проектирате робусност

Most bug backlogs are not really collections of isolated mistakes. They are symptoms of a system that makes the wrong state easy to create, hard to observe, and expensive to correct.

That distinction matters. Chasing bugs treats each incident as a local failure: add a condition, patch a query, catch an exception, deploy, repeat. Architecting robustness asks a harder but more valuable question: what property of this system allowed this category of failure to exist?

Robustness is not the absence of defects. It is the ability to behave predictably when inputs are incomplete, dependencies are slow, messages arrive twice, deployments overlap, and someone eventually uses the API in a way its original author did not expect.

Design for failure paths first

Happy-path code is usually short and obvious. A PHP endpoint receives valid JSON, a database write succeeds, and a downstream API responds immediately. The production system, however, lives in the branches around that path.

Start by making failure states explicit. If an order cannot be charged, should it remain pending? If an email provider times out after accepting the request, is retrying safe? If a cache is unavailable, can the application use the database without overwhelming it?

These are product and architecture decisions, not merely exception-handling details. They deserve names, states, and tests.

try {
    $payment = $paymentGateway->charge($request);
} catch (GatewayTimeout $e) {
    $order->markPaymentUnknown();
    $orders->save($order);

    return new JsonResponse(['status' => 'processing'], 202);
}

The important choice here is not the catch block. It is refusing to pretend that a timeout means a declined payment. An explicit “unknown” state gives a reconciliation job, an operator, or a later webhook somewhere safe to continue from.

Make operations idempotent

Retries are unavoidable in distributed systems. Browsers retry requests, queues redeliver messages, workers restart, and network failures leave callers uncertain whether a request completed. If repeating an operation creates duplicate charges, duplicate records, or inconsistent balances, a transient failure becomes a data incident.

For externally triggered writes, use an idempotency key. Store it with the resulting operation under a database uniqueness constraint, then return the original result for repeated requests.

CREATE UNIQUE INDEX payments_idempotency_key_unique
ON payments (idempotency_key);

The constraint is essential. A PHP-level “check before insert” can still race when two requests arrive together. The database is the final authority for shared state, so let it enforce the invariant that actually matters.

Idempotency does not mean every endpoint must be identical under repetition. It means you deliberately define what repetition does. A request to create a payment may return the existing payment; a request to send an email may record a single intent and let a worker handle delivery; a request to increment a counter may need a different model entirely.

Put boundaries around data

Many backend defects begin when untrusted or loosely structured input moves too far into the application. Request arrays reach domain logic. Nullable database fields become implicit control flow. A third-party API response is treated as if it were your own type system.

Validate and normalize at the boundary. Convert incoming data into a small, well-defined representation before business rules execute. Reject invalid input with a useful client response; treat unexpected dependency responses as operational failures that need logging and recovery.

  • Validate required fields, formats, ranges, and allowed values at the API boundary.
  • Use parameterized queries or an established database abstraction for every query.
  • Keep database constraints for rules that must remain true regardless of application code.
  • Represent meaningful lifecycle states explicitly instead of scattering boolean flags.
  • Version public API contracts when a change cannot remain backward compatible.

A database constraint is not duplicated validation. Application validation improves feedback. Database validation preserves truth when data arrives through a worker, an admin tool, a migration, or a future service.

Make dependencies finite

A downstream call without a timeout is an invitation for one slow service to consume all available web workers. A retry without a limit is an outage amplifier. A queue without a dead-letter strategy can hide poison messages forever.

Every dependency needs a bounded failure policy: connection timeout, total timeout, retry conditions, retry limit, and a result when retries are exhausted. Retry only failures that may be transient, and make the retried work idempotent first.

For a database-backed application, this also means protecting connection pools and query cost. Add indexes based on actual access patterns, inspect slow queries, and avoid loading entire result sets when pagination or streaming is sufficient. Caching can reduce repeated work, but it should not be the only thing preventing a basic query from collapsing under normal demand.

Observe the system you actually built

Logs that merely say “something failed” turn every incident into archaeology. A robust service emits enough context to reconstruct an individual request without exposing secrets: a request ID, operation name, outcome, relevant resource identifier, duration, and dependency status.

Metrics should answer practical questions: Are errors increasing? Are requests slower? Is the queue growing? Are retries succeeding or accumulating? Structured logs and metrics do not eliminate failures, but they shorten the distance between an alert and an informed decision.

Do not log passwords, tokens, authorization headers, or full payment details. Observability should increase operational clarity, not create a second security problem.

Deployments are part of the architecture

Code can be correct and still fail during release. Schema changes, background workers, application containers, and old application versions may coexist briefly. Treat deployments as compatibility windows, not instantaneous swaps.

A safe migration sequence usually expands before it contracts: add a nullable column or new table, deploy code that can read both shapes, backfill if needed, move traffic or behavior, and only later remove obsolete fields. Avoid a deployment that requires every process to switch at the same instant.

Docker helps make runtime environments repeatable, but a container image does not remove operational design. Configure health checks that reflect readiness, provide configuration through the deployment environment, run database migrations deliberately, and ensure failed containers can be diagnosed from their output rather than by entering a mutable server.

Turn incidents into design improvements

After a defect, the fastest fix may still be necessary. But the follow-up should identify the missing guardrail: a constraint, timeout, state transition, test case, alert, ownership boundary, or deployment rule.

The best engineering teams do not become robust because they predict every future bug. They become robust because each failure teaches the system how to fail more safely next time. Stop measuring progress by how quickly patches ship. Measure it by how many recurring classes of failure the architecture makes difficult, visible, and recoverable.

Портрет на автор на блогот

Mihajlo

Јас сум Михајло - развивач поттикнат од љубопитност, дисциплина и постојаната желба да создадам нешто значајно. Споделувам увиди, упатства и бесплатни услуги за да им помогнам на другите да ја поедностават својата работа и да растат во постојано развивачкиот свет на софтверот и вештачката интелигенција.