Системска архитектура како жив документ: Еволуирање надвор од статичните дијаграми
A system diagram is often born during a moment of clarity: a few boxes, a few arrows, and a shared sense that everyone finally understands how the application fits together. Then the system changes. A cache is added, a background worker appears, an API gains a second consumer, and one database table becomes three. The diagram remains untouched because updating it feels less urgent than shipping the next feature.
That is how architecture documentation becomes decorative. It is technically present, occasionally useful, and increasingly disconnected from the system developers actually maintain.
Living architecture documentation takes a different approach. It treats architectural knowledge as something that evolves alongside code, deployment, operational practice, and team decisions. The goal is not to document every implementation detail. It is to preserve the decisions and boundaries that help people change the system safely.
Static diagrams fail for predictable reasons
A single high-level diagram cannot answer every architectural question. It may show that a PHP application talks to PostgreSQL and Redis, but not whether writes are synchronous, which service owns a piece of data, or what happens when a queue consumer is unavailable.
The problem is rarely that teams do not value documentation. More often, the documentation is too expensive to maintain or too broad to be useful. A diagram maintained in a design tool may require a separate workflow, special access, and someone who remembers every recent change. Under delivery pressure, it loses.
Documentation also becomes stale when it tries to capture the wrong level of detail. A diagram containing every controller, table, and container is difficult to read and almost guaranteed to drift. Architecture should explain significant structure, not reproduce the repository in graphical form.
Document decisions, boundaries, and change paths
The most durable architectural documentation answers practical questions. Where does a request enter? Which component owns the business rule? Which database is the source of truth? What can fail independently? How is a change deployed and reversed?
For a typical PHP backend, that might mean documenting a boundary between HTTP delivery code and application logic. A controller should translate a request into an application command, while a use-case class coordinates validation, persistence, and events. The exact directory names matter less than the dependency direction.
final class CreateInvoiceController
{
public function __invoke(Request $request, CreateInvoice $createInvoice): Response
{
$invoice = $createInvoice(
new CreateInvoiceCommand(
customerId: $request->get('customer_id'),
amount: $request->get('amount')
)
);
return new JsonResponse(['id' => $invoice->id()], 201);
}
}
That small example communicates an architectural choice: HTTP concerns do not belong inside the invoice workflow. When a queue consumer, CLI command, or internal API later needs to create an invoice, it can use the same application boundary without simulating an HTTP request.
A living document should state this choice plainly, including exceptions. If legacy controllers still contain business logic, say so. Honest documentation is more valuable than an idealized picture of a system that does not exist.
Keep architecture close to the code
Architecture changes should be reviewable in the same pull request as the code that introduces them. This does not mean every code change requires a diagram update. It means significant changes should leave a lightweight trace in the repository: a concise decision record, a component diagram expressed as text, or a focused update to an operational runbook.
Text-based diagrams work well because they can live with the code, be reviewed in diffs, and be updated without a separate tool. Their value is not that text is superior to visuals; it is that the maintenance path is short.
Browser -> PHP API
PHP API -> PostgreSQL
PHP API -> Redis
PHP API -> Queue
Worker -> Queue
Worker -> Email provider
Even this simple view becomes more useful when accompanied by a few notes: PostgreSQL owns invoice state, Redis is disposable cache data, the queue carries retryable notification work, and the email provider is an external dependency with failure handling.
Use decision records for the “why”
Code shows what the system does now. A decision record explains why a consequential choice was made and what trade-off was accepted. Keep these records short. A useful record usually includes the context, the decision, consequences, and the date it was accepted.
For example, a team might record that invoice creation remains synchronous because the caller must receive a definitive identifier before continuing, while email delivery is asynchronous because temporary provider failures should not block the transaction. That distinction prevents a future “performance improvement” from moving the wrong work into a queue.
Decision records should not become a ceremony for naming variables or reorganizing folders. Reserve them for choices that affect reliability, ownership, security, deployment, data consistency, or future options.
Make runtime behavior visible
Architecture is not only a set of components. It is also behavior under normal and abnormal conditions. A request flow may look simple until a database timeout, duplicate message, failed deployment, or cache miss occurs.
Document the important failure paths. If a worker retries jobs, define where retry limits and backoff live. If a payment callback can arrive twice, identify the idempotency key and the database constraint that protects it. If Redis is unavailable, state whether the API degrades gracefully, bypasses cache reads, or rejects the request.
These details are especially important when Docker is involved. A local docker compose environment can show developers which services are required and how they connect, but it should not imply production guarantees that do not exist. Document health checks, environment variables, persistent volumes, and startup dependencies separately from assumptions about a production orchestrator.
- Describe the owner and source of truth for each important data set.
- State which operations are synchronous and which are queued.
- Record retry, timeout, and idempotency behavior for external interactions.
- Link deployment and rollback steps to the components they affect.
Review architecture through change, not ceremony
The best time to update architecture documentation is when the team already has the relevant context: during implementation and code review. A practical review prompt is simple: did this change create a new dependency, alter a trust boundary, move data ownership, introduce a failure mode, or change deployment behavior?
If the answer is yes, update the smallest useful artifact. Adding a read replica may require a component note and a decision record. Changing a query index may only need a migration and performance-focused documentation near the query. Not every change deserves a diagram.
Architecture reviews also benefit from deletion. Retire diagrams that duplicate better documentation, replace vague pages with sharper ones, and mark known inaccuracies until they can be corrected. A smaller set of trusted documents beats a large archive of uncertainty.
The document is the shared model
A living architecture document is not a polished artifact produced after the real work is finished. It is part of the engineering system: a shared model that helps developers reason about consequences before they make a change.
When documentation evolves with the software, onboarding becomes faster, incidents become easier to investigate, and technical debates become more concrete. The system will still change in surprising ways. The difference is that the team has a reliable place to capture what changed, why it changed, and what must remain true next time.