April 3, 2026
3m 37s

In one system I worked on, the team had a clear goal of making the software portable across platforms:
Different customers wanted different targets.
So the team did what teams often do. They built a comprehensive OS abstraction layer. Threading, memory management, synchronization, timing; everything was wrapped behind a clean interface.
It was a great idea with good intentions, but also a real investment in time and complexity.
The team spent significant effort building and maintaining the abstraction layer, and every time a new platform or OS was introduced, it had to be expanded further. In practice, it made the system harder to understand, harder to debug, and harder to port.
That also meant additional testing and regression effort to ensure the abstraction continued to behave correctly across platforms.
The abstraction layer accumulated layers of complexity, almost like a Coral Reef.
That experience led me to a simple conclusion: The OS is the wrong thing to abstract.
Threading and scheduling can be treated as implementation details at the level of individual components, but their effects on system behavior cannot be hidden.
When you hide them behind an abstraction layer, you don’t remove complexity. You move it out of sight.
Now your system has:
And in embedded systems, execution doesn’t just come from threads. Work is often kicked off by interrupts, introducing additional entry points with their own timing constraints.
If your architecture doesn’t make these paths explicit, the system becomes harder to reason about.
The same system also had a data pipeline. Data arrived in bursts from external hardware, was decoded, and pushed through processing stages.
A natural fit for an event-driven design!
But the real behavior of the system wasn’t defined by events. It was defined by:
The problems didn’t show up in threads or mutexes. They showed up in:
By abstracting execution, the system made these behaviors implicit and harder to analyze.
This same pattern shows up in hardware abstraction layers (HALs).
Many HALs focus on normalizing how you talk to hardware:
At that level, the goal is consistency. Different platforms expose different registers and APIs, so the HAL provides a uniform interface.
But that’s not where systems fail.
The application doesn’t care how to set or reset a pin, and in many cases, it doesn’t even care that a pin exists.
It cares about what the system is doing:
A HAL that abstracts register access still leaves the real behavior implicit.
It can make drivers look clean, but it doesn’t make the system easier to reason about. It often pushes complexity up into the application, where it becomes noise.
It helps to separate two very different goals.
Interface abstraction focuses on mechanisms:
Behavior-oriented abstraction focuses on meaning:
Many OS abstraction layers and HALs stop at the first level, but the real complexity lives in the second.
In my own work, I’ve moved toward making system behavior visible:
This shifts the focus from mechanisms to behavior.
Porting becomes more mechanical. You map threads to tasks, queues to RTOS primitives, interrupts to entry points, and device interactions to platform-specific implementations. You’re not rediscovering hidden execution semantics—you’re preserving an explicit design.
OS abstraction hides execution, while HALs hide plumbing. Neither addresses the level where systems actually break.
Abstraction isn’t the problem. Abstracting below the level of system behavior is.
If you want systems that are understandable, debuggable, and portable, the goal isn’t to hide the OS or the hardware.
The goal is to make execution and data flow explicit—and keep them that way.