Like so many terms in the software world, the terms “software architecture” and “software detailed design” do not have canonical definitions, i.e. every developer, company, etc. has their own definition. On many embedded project teams, there is no distinction—or at least not one that the developers can articulate—between the two. The tendency, then, is to define architecture and detailed design together. This works up to a point, but teams tend to focus on the detailed design, and the architecture essentially becomes the output of that detailed design. This leads to an architecture that is rigid in terms of dependencies and oftentimes inconsistent with itself.
The problem with code designed without an architecture document arises when you try to add new features that don’t quite match up with the original detailed design or when you encounter a scenario where you’re trying to shoehorn a “missed feature” into the design. For example, I worked on one project where the team designed the HTTP request engine to use synchronous and asynchronous inter-thread communication (ITC) to send requests to the external cell modem driver. Later in the project, we added a watchdog sub-system that would monitor the system for locked up threads, but we found that the watchdog would intermittently trip on the thread running the HTTP engine. The root cause turned out to be that, given a specific set of preconditions related to cellular network failures, the synchronous ITC calls from the HTTP request engine would block for minutes at a time. Nothing in the original design proscribed when synchronous ITC could (or could not) be used. Because we did not have a written software architecture, there was nothing to guide or constrain the design of this feature. The developer of the HTTP engine just threw something together that reflected his minimal understanding of cell modem behavior. Ultimately, we had to leave the watchdog sub-system out of the final product.
You always want to have a detail-agnostic software architecture that the detailed design must conform to. It’s the difference between driving a car on a paved road with guard rails and driving through an open field. Yes, the paved road has constraints on what and when and how vehicles and people can travel on it, whereas the open field has none; but getting from point A to point B is a lot faster and safer on the paved road as opposed to crossing an unbounded open field where nothing prevents you from colliding with other vehicles or local wildlife.
Software architecture best practices are strategic in nature. Define your project’s software architecture first. Keep it separate from the software detailed design. There is an implied waterfall process here, but it’s a good thing. Organically derived software architecture is the path to the dark side; or, without the moral overtones, it is often a quick path to “bit rot.” Up-front architecture—separated from design—allows for just-in-time design, which is what you want in a development process like Agile. For example, if your software architecture defines the interface between the core business logic and the user interface as model points, then any work you do on the UI stories is completely decoupled from the business logic stories and vice versa. Only the model point instances need to be defined up front.