When you work on a single product for years, it is easy to structure the code around that one product and call it good. In contract embedded development, that approach breaks fast. Different clients, different MCU families, different communication protocols, different safety requirements. And all of them need maintenance in parallel.
Modularizing the source code is what makes that manageable. Not just cleaner to read, but genuinely easier to maintain, port, and reuse across projects.
The Real Cost of Fewer Files
Some developers prefer keeping the file count low. The argument usually goes: fewer files, less overhead, easier to navigate. It sounds reasonable until you have to work on a codebase written by someone else: five source files, tens of thousands of lines each, no clear structure, no coding standard. Getting in there to fix a bug or add a feature is not faster because there are fewer places to look. It is slower, because everything is tangled with everything else.
The complexity that matters to the developer is in the source code. How readable it is, how isolated each concern is, how easy it is to change one thing without breaking another. A project with fifty well-defined files, each with a clear purpose and a reasonable number of lines, is far easier to maintain than one with five enormous ones.
This is especially true when the same project needs to build on different toolchains. Not every MCU family shares an IDE, and even when cross-platform editors exist, specialized vendor tools still offer capabilities that general editors do not. A well-modularized project adapts. A monolithic one requires surgery every time the environment changes.
Four Layers That Work
The structure I use consistently across projects has four layers: applications, modules, drivers, and libraries. It is not the only valid approach, but it has worked reliably across different platforms, clients, and toolchains.
Applications are the top-level programs that run on the device. A product typically has more than one: the field application, the bootloader, the manufacturing test firmware. Each can be a completely separate IDE project, or coexist in the same environment using different subsets of the layers below. Either way, they are built from the same underlying modules and drivers.
Modules are the "business logic", a term borrowed from the software world and increasingly adopted in embedded development. They monitor, process, and decide. A module receives data from drivers or libraries, applies the rules the product requires, and triggers the right actions. This is the layer where the product's actual behavior lives, decoupled from the hardware.
Drivers sit between the modules and the hardware. They abstract hardware access so the modules do not need to know whether they are talking to one MCU family or another. A well-written driver can be copied into a new project, its hardware access functions updated, and everything built on top of it continues to work. This portability is exactly what manufacturer HAL and LL libraries aim for. The driver layer extends that idea across the full project.
Libraries are the support utilities: math functions, protocol parsers, ring buffers, CRC routines. A mix of your own code and third-party components, with no hardware dependency and no product-specific logic. They are the easiest layer to reuse and accumulate over time into a toolkit that accelerates every new project.
The Connection That Took Me by Surprise
When I was studying engineering, I had a networking course that covered the OSI model. At the time I thought it had nothing to do with me. I was on the electronics side, not software or networks. Years later, when I started structuring firmware architectures seriously, I realized the OSI model had already given me the mental framework I needed.
Not because firmware implements OSI. It does not. But the principle is the same: each layer has a defined responsibility, communicates through a defined interface, and does not need to know how the layer below it works. That separation is what makes a layered architecture maintainable, testable, and portable. A concept I thought I would never use turned out to be the clearest explanation I had for why modularization works the way it does.
Worth the Investment
Setting up a modular architecture takes more thought upfront. The file count goes up. The structure needs to be documented and applied consistently. But across a portfolio of projects on different platforms, the time recovered during maintenance, integration, and porting more than compensates.
Beyond time savings, working with code that has already been validated in real projects reduces testing effort on every subsequent project. Less rework, more predictable timelines — which directly benefits the product owner, not just the development team.