Legacy

How do I map the architecture of software that already exists?

Five passes, and a short list of what to record for each element so the map survives its second update.

Short answer

Map an existing system in five passes: what runs and where, the inventory of components, the relationships between them, the outside edges, and what could not be established. Record for each component a type, a readable purpose and a stable identifier — because a map whose components cannot be recognised in the next analysis is two unrelated lists rather than a history. Do not draw a diagram until the list exists.

#The five passes

  1. What runs, and where

    Environments, what is deployed to each, what runs on a schedule, and what runs that is not in the repository. From the cloud console and the crontabs, not from a conversation.

  2. The inventory

    Every component with a canonical type and one readable sentence. Services, modules, databases, queues, scheduled jobs, front ends, external systems.

    Canonical type matters: free-text kinds mean two analyses of the same system produce lists that cannot be compared.

  3. The relationships

    What uses, depends on, reads from, writes to or implements what — and in which direction. Direction is not decoration; it decides what breaks when something stops.

  4. The edges

    Interfaces exposed and consumed. For each: the route or channel, what it is for, how it authenticates, and what happens when it fails.

    The last two are where a plausible implementation and a correct one look most alike.

  5. What could not be established

    Every question the passes above raised and did not answer, with a reason and a priority. Recorded on the map rather than left out of it.

#What to record per element

The fields that make an element usable later
FieldWhy it earns its place
TypeCanonical, from a fixed set — so two maps of the same system are comparable.
NameWhat people call it. Useful, and not identity.
PurposeOne or two sentences a non-programmer can read. This field decides whether the map is usable by the person paying for it.
LocationA relative repository path, where one exists. Never absolute.
Stable identifierSee below. The field everybody forgets and the one the map depends on.
ConfidenceSo a guess does not read as a fact.
EvidenceWhere it was established — a file and a line range. Turns the map from testimony into something checkable.

#The field everybody forgets

A map without stable identity works perfectly the first time and stops working on the second update.

To say later that a component changed — rather than that one vanished and another appeared — the two versions of the map have to agree that they are describing the same thing. Names cannot carry that:

  • Rename a class and it is still the same class.
  • Call two things "Main database" and they are still two different things.
  • Move a file and every path-based identity breaks at once.

So give each element something stable to be recognised by: a repository path, a fully qualified name, or a label you choose and keep for things with neither — a database, a queue, a deployment target, a cloud resource. An element with none of the three is a new thing every time the map is rebuilt.

Three components, three kinds of identity Invented example — not a customer
SERVICE     Billing service
            path: app/Services/Billing/
            fqn:  App\Services\Billing\BillingService

DATABASE    Main database
            external id: db.main
            (no path, no qualified name — the label is the identity)

QUEUE       Payment queue
            external id: queue.payments

The two external identifiers are chosen once and never changed. That is the whole discipline: if somebody renames the queue in the infrastructure next year, the label stays queue.payments and its history survives.

#One system, five hierarchies

A component belongs in more than one tree at once, and flattening them is what makes most architecture documents feel almost right.

A payment service sits inside an application, inside a deployment, and inside a business capability. Those are three different parents, and forcing them into one hierarchy loses information every time.

  • Application — what the software is made of, in software terms.
  • Business — what it does, in the company's terms.
  • Organization — who owns what.
  • Deployment — what runs where.
  • Data — what information exists and where it lives.

You do not need all five on day one. You do need to record containment as belonging to a named view rather than as a single tree, or the second view is a rewrite rather than an addition.

#When to draw a diagram

After the list exists, not before. A diagram drawn first becomes the thing people argue about, and it encodes decisions — what to include, what to merge, what to leave out — that should be made from evidence rather than from what fits on a slide.

Once the list exists, a diagram is genuinely useful and cheap: it is a view of something you already have. Two rules keep it honest:

  • Date it and say what it was derived from. An undated diagram is unfalsifiable.
  • Do not let it become the source. When the diagram and the analysis disagree, the analysis is the one with citations attached.

#Keeping it true

A map is only worth having while it is still true, and the failure is always the same: it is produced once, as a project, and then nobody owns updating it.

Two things prevent that:

  1. Make rebuilding cheap. If updating the map costs an afternoon of somebody's attention, it will not happen. If it costs one instruction, it will.
  2. Rebuild on events, not on a calendar. After a release, at the end of an engagement, when a subsystem lands, before a handover. A quarterly cadence produces updates nobody reads and misses the ones that mattered.

The value is then in the difference between two versions rather than in either one — what arrived, what changed, what went away, and which earlier statements are no longer supported.

#What goes wrong

Drawing before listing.

Instead The list is the artefact; the diagram is a view of it. Drawing first encodes decisions nobody made deliberately.

Free-text component types.

Instead A fixed vocabulary. “Service”, “svc”, “microservice” and “backend service” are four names for one thing and make two maps incomparable.

No stable identifier.

Instead Path, qualified name, or a chosen label. Without one the map cannot have a history.

Omitting what could not be established.

Instead Record it. A map with no gaps has either come from a complete analysis or has quietly turned its gaps into silence.

One giant hierarchy.

Instead Record containment per view. A component legitimately has different parents in the deployment and in the business.

#What a map cannot tell you

What this does not do

  • Why it was built this way. A map records what exists; the reasoning lives in people.
  • Whether the architecture is good. There is no quality judgement and no score.
  • What happens at runtime — load, latency, what a specific user sees.
  • Anything configured only outside the repository, which appears as an open question rather than as a component.
  • Whether it is secure. A map is not a vulnerability assessment.

Passes two to five, derived rather than drawn.

One read-only instruction produces the inventory, the relationships, the edges and the explicit gap list — with a citation behind each claim so a new team can check rather than trust.

Build your project map — free See what one looks like