Software architecture is really similar in my view as architecture of buildings. You can build a small house without a well-planned architecture, but it will never be perfect. And you can not build a big building without good plans. During the planning phase you need to take care about several thinks: choose the correct walls, plan the electricity, water-system, all rooms, light system, air-conditioning and heating and a lot more. So this is a complex, difficult but necessary job.
It is the same at software architecture: you can implement small programs without a proper architecture, but they will be never perfect and you won’t be able to build up and maintain a huge program, because it will become a big monster with a lot of unknown code part, unknown functionality and bugs. And after a while at fixing each bug you are covering five other bugs. But of course it is not easy to plan the correct software architecture, you need to think about several points: main functionality, hardware requirements (memory usage etc.), define the main components, reusability of the code, stability, timing, internal communication, compatibity, extensionability, configrability etc. etc. etc.
And what are you doing in fact when you are planning your software architecture: based on the requirements you are splitting the whole software into components and subcomponents, planning their responsibilities and interfaces and planning the way of communication between the components.
There is one more big difference between the architectural plan of a building and software architecture: since the building and not changing frequently the software is changing: the requirements are changing often and new functionality is coming. So you need to plan your software in a way that it is possible to extend it without big changes. For that you need to find a really modular architecture.
I have one more favourite explanation of good software architecture: a nice modular software architecture is just like a furniture in IKEA (no, it is not an advertisement here). That furnitures are built up from really simple modules (modular built-up and components with single responsibility), which are connecting to each other at some predefined connection points (clear interfaces) and they are documented in a pretty easy and pretty clear way (well documented usage). Furthermore there are several variant of each furniture, you can choose between glass door and wooden door, 3 or 4 chest versions, add a lot of extensions like extra lights or a mirror inside. So it is open for extensions and it is configurable. So my goal at desigin a software is always a similar result.
Here I just tried to collect the most important points of good software architectures.
Your software needs to be built up from well-defined modules/components. If your component is still to big you can split it into so-called subcomponents. In case of a complex software you can use multiple level of subcomponents. As you are using this strategy you will have a lot of reusable components.
*Clear responsibility for the components
Each of your components needs to have a clear responsibility. It is good is you can describe its responsibility with one sentence. If you can not describe it without using the words “or” and “and” then your component is too big as it should be splitted. Good examples for the responsibilities: “This component is responsible for the storage of user data.” . But even inside this component some subcomponents can be introduced, like a component responsible for the connection with the database.
The defined components needs to communicate with each other through clear interfaces. Each interface has a name, a return type (it can be void as well), and optionally some parameters. That a good practise to make a difference between private and public interfaces. Private interface is available only inside the same component, public interfaces are available for the other components as well. The number of public interfaces should be as limited as possible. Each public interface needs to be well-documented. On long term you should avoid to change or remove already existing interfaces.
Your component will be used by other components and so maybe by other programmers. So it should be documented in a very clear way what are the components doing and what is the expected behaviour of their interfaces. You can change the internal behaviour of your component any time (how is it doing), but avoid to change the external behaviour (what is is doing). For this purpose you can use UML diagrams.
*Easy to understand
I have seen several times architectural designs with very long chain of derivation and complicated connection between components. In the end they were working perfectly, but no one understood why and how to use them. You should avoid such solutions. Since your component will most likely be used by other developers it should be easy to understand its behaviour.
Your component should be well configurable to be able to use it in multiple environments. My strategy for making my code configurable is the following: everything what would be a magic constant in the code (path, magic string, magic numbers etc.) should be moved to a configuration class which is storing all such information. And then this information should be able to be modified from outside through a config file.
In case of a complex software not all components should communicate with all the others directly. You can introduce some additional restrictions, like layers. You should organise your components into layers and only the once can directly communicate with each other which are either in the same layer or in the neighbour layer. A good example is the so called MVP (Model-View-Presenter) architecture which is often used for GUI applications. The view can not communicate directly with the model, but the presenter can communicate with both of them. With this solution your way of communication will be much more clear and you can change your components much easier.
*Clear communication sequence
It should be defined in a clear way how your components are communicating with each other. How is the information flowing between them. Which one is calling which one. For the purpose you can use a UML sequence diagram.
*Well-designed error handling
The error handling of your whole software should be handled based on a common concept. There are several solutions: using special return values, exceptions or error flags, but what is important, that you need to use the same solution in all of your components. That makes it easier to understand your whole system.
These 9 points are not covering everything, software architecture is a much more complex topic. But if you are taking care about these 9 points at planning your software you will already reach a good software architecture.