Software architecture is the set of decisions that are expensive to reverse once a system is live, and what marks one is that price, not its size on a diagram. Timing sets that price: an assumption closed before the code exists turns later work into definition, while the same assumption arriving late must be retrofitted onto records already written. Under the Vipkurum roof no student or payment record ever reaches a neighbouring institution.
Which decisions you cannot take back
You find out which decisions were architectural when you inherit someone else’s system. What you are free to change is what the earlier decisions left open.
Take a line drawn before there was a core to hold it. Seven institutions run under Vipkurum, each with its own students, timetable and payment records, and none of those records ever surfaces inside a neighbouring institution. That rule lives in the data model rather than in the interface: the moment a record is written, it already belongs to a tenant. A check bolted onto the interface later cannot carry the same weight, because the record itself does not know who owns it. A screen can hide it; a report cannot. Draw the line once the platform is live and you are reassigning ownership across every record already written.
A connection assumption sits in the same class. AlgoKlima’s field app never waits on a network, so a job opened in a boiler room or a lift shaft lands in the phone’s own queue first. That decides where a record is first written, which is why it cannot be bolted on later. Making a server-first app work offline is not a matter of adding a screen; it moves the place the record is first written, and with it the record’s identity and its ordering.
The quietest one is assessment. Vipkurum lets an institution define the assessment type of its own exam. Hard-code that instead and European and Turkish criteria cannot sit in one exam infrastructure; repairing it means rewriting the exam engine.
The test also works in reverse. Screen layout, report columns and the shape of a panel’s menu take up space on a diagram and can still be changed next week; as long as they leave the record structure alone, they are preferences rather than architecture. The measure is one question: how many existing records would have to be redefined to undo this? If the answer is none, the argument is about taste, and keeping the two apart leaves a handful of real calls to settle before code.
Write the boundary as a sentence someone can break
Before any code exists, we reduce every boundary to a single sentence: the one statement the system is never allowed to violate anywhere. It has to work for the person who does not write software and the person who does: one reads it and knows what was promised, the other makes it visibly false the moment they break it. A diagram stays quiet when it is violated.
These are sentences from systems we run.
Vipkurum: no student, guardian or payment record crosses a tenant boundary. A report that reaches into a neighbouring institution’s data breaks that sentence, and the break shows.
AlgoKlima: the field app is a logbook that works on its own, not a form waiting on a connection. The record goes into the phone’s local queue before it goes to a server.
Dental clinic CRM: an appointment points at a specific step of the treatment plan. Change the step or add a session and the calendar moves with it, so no appointment is left pointing at nothing.
Resterm’s heater selection software: you can read a quote line backwards to the heating requirement it came from. The heater the calculation picks goes into a basket, the basket into a project, the project onto a customer, and the records stay on one chain. If a figure gets retyped anywhere along it, the sentence has already failed.
What they share is that each can be broken. That is the property we look for when we write one. A sentence nobody is able to violate is not a boundary; it is a wish.
Timing decides what stays changeable
The same call made early or late leaves you different room to move. Taken early, it turns future work into definition: the system already expects the case, and a new request produces records instead of code. Taken late, it is bounded by whatever gap the existing stack has left. Every row below comes from our own work; the columns hold what happened.
| When the decision was made | What stayed possible afterwards | Where |
|---|---|---|
| The tenant boundary was drawn into the data model while the core was built | Adding an institution became a tenant definition, not a new installation | Vipkurum |
| The connection assumption was rejected while the mobile side was built, along with the rule that the queue drains in creation order | Technicians logged work outside coverage; conflicting edits merged into one record | AlgoKlima |
| Stock movement was tied to the work order while the data model was built | The invoice came off the same record; cost and revenue never split across two programs | AlgoCar |
| The broadcast layer is going in before the clients multiply; the product is under construction | Target: web and mobile clients listen to one Reverb broadcast, with no second update path | Secretum (in development) |
| Storage layout and caching reached us with the system already live | Work stayed where the page weight was, inside the stack already in place | KGM SsangYong corporate site |
The last row is different. The decision was not taken late; it reached us late. The system was already serving traffic and had sections that worked. On a system in production, the architectural question is no longer which design is right, but how much room to move is left.
The third row shows the same mechanism on the accounting side. In AlgoCar, the rule that stock movement comes off the work order was settled while the data model was being built, so invoicing from the same record needed no bridge afterwards. Settle it late and the link between work order and invoice has to be drawn across records that already exist; for the older ones it can only be guessed.
Draining the queue in creation order belongs on the early side too. Choose that rule afterwards and you are deciding which of two conflicting edits wins across records that already exist, without a dependable account of when each was made.
How many surfaces will read the same core
The number of interfaces a system will have looks like a question about screens. It is not. It sets what the core owes each audience and which paths may update data, and once the core exists it is among the most expensive assumptions to change.
On Tahlil.com the question closed at the start. The patient site where results are entered, the panel where a physician writes the reading, and the app that calls the patient back once a reading is approved all sit on one Laravel core. MySQL holds the records, Redis carries the cache, OneSignal pushes the notification when a reading is ready. Every surface reads the same data model; what differs is permission.
AlgoKlima draws the same line between the office and the field. The office panel runs on Laravel with Inertia, the field app on React Native (Expo). Neither keeps its own copy: both read the same customer and device record from the same MySQL core, and edits arriving from the field and from the office merge into a single record through conflict resolution. Keep separate copies and the rule deciding which copy wins has to be written into the middle of the system later.
In Secretum, which we are building now, we are binding the same call before the clients multiply. One Reverb broadcast set up on the Laravel side feeds both the Vue 3 web interface and the React Native (Expo) client, and no second update path is written. Open a second path and every new feature has to be updated in two places.
There is a counter-case. Not every system needs several surfaces, and building for a second one where a single panel does the job is cost without a return. The rule is simply that the question belongs before the core.
Derived record or a second entry
When one fact lives in two places, the two drift apart eventually. The decision that prevents it is structural rather than technical: on day one you name which record is the source and which is derived.
In AlgoCar, parts consumption comes off stock the moment it is written onto the work order. Nobody keys the warehouse movement a second time on a separate screen. The invoice is raised from that same record, so work order, parts consumed and invoice travel on one chain. The cost and the revenue of a job are never reconciled between two programs, because they were never apart.
The construction workforce attendance system applies the same split to wages. Earned wages are not held in a separate spreadsheet; they are produced from matched clock-in and clock-out pairs. The QR code establishes who scanned and the location establishes which site, and the two arrive as one entry record. An entry left open stays visible as an incomplete record until its exit is scanned. When someone disputes a figure, the thing you open is the source pair itself.
The counter-case is sharp here. A derivation chain can only be built when the source record is created inside the system. On an inherited system where the source is created elsewhere, in another program or in a sheet somebody maintains by hand, building the chain afterwards means rewriting the system. Answer the question late and there is no answer left to give.
Inherited systems come with inherited limits
Most people reading this are not starting from an empty repository. They have something that works, has aged, and that they would rather not rewrite. The KGM SsangYong corporate site arrived that way.
The order was: read the codebase, leave the working parts alone, find where the weight sat. On an inherited system the first architectural task is not drawing a new design; it is writing down which constraints you are borrowing. Ours was that nothing we did would change how the site already behaved, and that shaped the sequence: measure first, touch second.
Catalog reads moved onto a Redis cache, so the same query no longer runs once per visitor; MySQL wakes only when the underlying data changes. Vehicle images moved into one naming and folder convention, so you can tell which model a file belongs to without opening the code. Neither needed a rewrite; that was the room the earlier decisions had left.
The framework was the closed decision. Work stayed inside the existing Laravel, Tailwind and jQuery set, and nothing but the cache went into it. Taking a system over and improving it is how most of our software consulting work starts: something already runs, and the question is not what should replace it but what can be done inside it. What happens after that is a separate call, and we take it up in the piece on who maintains software after launch.
Picking a stack is an architecture decision
A stack usually gets filed under taste. We treat it as one of the hardest decisions to reverse, because it settles who can keep the system alive.
We do not pick a new stack per project. We pick one that can be maintained for years: Laravel, MySQL and Redis at the core, Inertia and Tailwind at the surface. Every phone app that leaves this desk is React Native (Expo) talking to that same core, which is why the AlgoKlima field app and its office panel read one customer and device record instead of separate copies.
Specialist runtimes come in only when the problem forces them: Python and OpenCV for image processing, Gemini or OpenAI when a generative model is needed. The optical mark and question-cropping tools built under the Vipkurum roof came out of that exception. The exception has a rule of its own: the specialist tool solves the problem and hands back, and the form it reads still lands in the same record structure.
Keeping the stack narrow is about maintenance, not preference. Stepping outside the tools the maintaining team already knows is among the most expensive moves to undo. SsangYong showed the argument from the other side: a new dependency would have made that week easier and moved the site outside the stack it lives in. The set held, and the work finished inside it.
What we settle before the first line
The list we close before code is fixed, and it runs in this order.
The system map comes first: data sources, actors, integrations. On Tahlil.com it put every surface on a single Laravel core, and that is where it became clear which surface reads which record under which permission.
Then we walk the high-risk path. On the same project the system drafts a preliminary reading, but the last word on anything a patient sees stays with the physician. That is a responsibility boundary, a different class of decision from a data boundary: one says where a record may travel, the other says who signs off on it. It was settled before code, because approval is the flow itself rather than a screen added to it.
The order is not arbitrary. Write a boundary sentence before the map exists and the rule ends up phrased in the language of screens, so it loses its meaning as soon as a screen changes. The risky path comes early for the same reason: the most expensive assumption sits there, and if it is wrong it moves the map with it.
Constraints go down next: who maintains this, what environment it runs in, what has to keep working. On inherited work those questions are the architecture.
Last, every boundary gets reduced to one sentence. If breaking the sentence would not be visible, the boundary has not been written yet.
The three calls we bind early
New build or inherited system, these are the decisions we make first.
- Every boundary comes down to one sentence. A boundary that stays invisible when it is broken is not a boundary; the tenant rule in Vipkurum is the clearest version of it.
- Assumptions about connection, tenancy, responsibility and surface count close before code. AlgoKlima’s local queue is that decision in working form.
- The stack is chosen from what the team keeping the system alive already knows; on an inherited system that call sets how much room you have.
Those are the first items on the table in a system architecture engagement. Everything else, patterns, folder layout and whichever framework is in fashion, can change later. These cannot.