HORUS/blog

Sep 5, 2026 · middleware · ros2 · team-decisions · robot-architecture

Why Middleware Decisions Are So Hard to Reverse

Middleware choices stick because they spread into drivers, tools and hiring. Here is how much room you have left, and when a switch is worth paying for.

Middleware decisions are hard to reverse because picking ROS 2 or HORUS shapes your code, your tools and your hiring, not just your message passing. Every driver, every launch file, every debugging habit and every new hire is bought against that choice, so the cost of leaving grows with the project. The exception is a robot still small enough that one person could rewrite it in a fortnight. The rest of this post is for a team about to commit, or already regretting a commitment, and trying to work out how much room is left.

The decision took an afternoon. Someone asked what the robot was going to run on, someone else said the obvious answer, and by Friday three programs were talking to each other and everybody was pleased with the week. Nobody wrote it down as an architecture decision, because it did not feel like one. It felt like choosing a package.

A year and a half later it is the largest structural fact about the codebase. The camera driver you bought speaks that stack. Your build exists to satisfy it. Your dataset lives in its recording format. The two people you hired in spring were hired partly because they already knew the vocabulary, and the one who did not spent six weeks learning it.

Now something is going wrong that the stack happens to be bad at. Perhaps the control loop misses whenever the cameras are busy. Perhaps a licence question came back from your lawyer with an answer you did not want.

And the question in the room, the one people ask carefully because everyone knows the answer might be expensive, is whether you are still allowed to change your mind.

Why are middleware decisions so hard to reverse?

Because middleware is not a component your robot uses, it is the grammar every other component is written in. Every program is built as a citizen of the messaging system: it starts the way that system starts programs, finds its neighbours the way that system finds neighbours, describes its data in that system's message definitions, and fails in that system's characteristic ways.

Change the foundation and none of that survives untouched. Launch and configuration files, which quietly encode a great deal of hard-won operational knowledge, do not carry over. The drivers you did not write are the worst of it, because a sensor vendor shipped a package for one stack and has no commercial interest in the other.

Then there is the part nobody puts in the estimate. Your team's intuition about what a failure means belongs to the stack that produced it, and on an unfamiliar foundation a capable engineer is temporarily a beginner again. An entire team of temporary beginners is the real bill, and it appears on no comparison page.

What part of a robot project does the middleware choice actually touch?

More than the wire, which is what people picture and the smallest part of it. Middleware in robotics arrives as a bundle: a way for programs to find each other, a way to describe messages so two programs agree on their meaning, a way to start and supervise a set of programs together, a way to record and replay what happened, and conventions about time and coordinate frames that every package silently assumes.

Each of those is a hook into your code. The discovery mechanism decides how your robot behaves on a bad network. The launch system becomes your deployment story. The recording format becomes your dataset. The coordinate conventions become assumptions buried in geometry code nobody has read since the day it started working.

If any of those words are unfamiliar, the plain-English guide to robotics middleware is the better first read. The point here is that the choice was never one decision. It was five or six decisions delivered together, and you accepted all of them on the afternoon you accepted the bundle.

What does a stuck middleware decision look like from inside a team?

It looks like a list of small, sensible decisions that each made leaving slightly harder. Nobody ever decided to become dependent. Someone added a package because writing the equivalent would have taken a month. Someone chose the recording format because the visualiser already read it. Someone hired the candidate who could start on Monday.

The tell is a particular kind of conversation. An engineer proposes something, and the answer is not that the idea is wrong but that the stack does not do that, and everyone nods and moves on. It happens twice, then monthly, and then the idea stops being proposed. A foundation you cannot leave slowly becomes a foundation that defines what your team is willing to imagine.

The second tell is a workaround with a nickname. Every team that has outgrown its foundation has one piece of software whose only job is to compensate for the foundation, and it is always maintained by whoever was least able to refuse. Why projects outgrow their first framework is the longer version of this pattern.

What are your actual options once the choice stops fitting?

There are seven, and most are not a rewrite. The cheapest is to change what sits underneath: ROS 2 talks to the transport through an abstraction layer, so a different DDS implementation or a non-DDS option can be swapped in without node code changing. The next is to remove one process boundary that has become expensive, which does not touch the foundation at all.

Then there is running two stacks side by side with a bridge, which is how nearly every real migration happens and which turns a flag day into a slow drift. A different foundation is where HORUS sits alongside ROS 2 as a different answer to the same question: an open-source real-time robotics middleware for Rust, Python and C++ where all three languages share the same shared-memory ring buffers, so messages between processes on one machine are not serialised, published under Apache-2.0, validated in simulation, and carrying no navigation or mapping stack of its own.

The remaining options are blunter: write your own transport for one narrow path, build on a hardware vendor's SDK, or stay put and treat the ceiling as a business decision.

How do these options compare side by side?

Read the last column first. Most teams can strike three or four rows immediately, and the argument that matters is between the two that survive.

OptionWho it is forWhat it assumes you knowWhen to pick itWhen not to
Swap what sits under ROS 2Teams whose complaint is the transport aloneYour DDS vendor's configuration filesThe packages are fine and the wire is notNobody will own the tuning afterwards
Remove a process boundaryTeams with one hot pair of programsYour own code and where you split itTwo programs already live and die togetherParts must crash and restart separately
Two stacks and a bridgeTeams shipping now and rebuilding laterBoth stacks, and the cost of a translation hopNew work can start clean beside old workNo one person will own the bridge
HORUSTeams whose same-machine traffic is the limitRust, Python or C++, and your process splitSeveral programs on one board pass large dataYou need mapping and navigation packages
Full rewrite onto another foundationTeams early enough to absorb a stopThe old system honestly, and the new oneThe prototype has not yet grown customersField units already run the current build
Your own transportTeams with one narrow, permanent pathMemory layout, lifetimes and failure modesA single link matters and nothing else doesMore than one team will touch the code
A hardware vendor's SDKTeams shipping on one robot from one supplierThe vendor's tooling and release habitsThe hardware is fixed for yearsYou expect to change hardware or supplier

Are you a solo builder, a startup, or an established robotics company?

A solo builder is the only group that can genuinely reverse a middleware decision cheaply, and should therefore worry about it least. One person holding the whole system in their head can rewrite the plumbing over a couple of long weekends, with no coordination cost and no institutional memory to migrate. Choose what you enjoy working in and change your mind later.

A startup of roughly two to ten engineers sits in the dangerous window. The codebase is already bigger than any one head, but a migration eats every available hand for a quarter, and a quarter is a meaningful fraction of the runway. This is the group that should think hardest before adding a fourth dependency on a foundation nobody is sure about.

An established company with a shipped product is past the point where reversal is a technical question. Certification paperwork names the stack. Field units run it. The honest move here is almost never a switch but a boundary, drawn so the next product line can be built differently while the current one is left alone. How teams actually decide to switch covers what that looks like.

What hardware are you committing to at the same time?

The hardware usually decides more of this than the software argument does. A sensor with a vendor-supplied package for one stack and nothing for the other has quietly made your choice already, and no architectural preference outvotes a depth camera you have already bought. List every device on the robot and write next to each one who supplies the driver.

The compute layout matters next. A robot with one board running everything is a robot where all messaging is same-machine messaging, and that is exactly where a shared-memory foundation behaves differently from a network-shaped one. A robot split across a compute board and an operator laptop has a network link in the middle that no shared-memory arrangement touches.

Then there is the split that dissolves the question. Many robots put the tight motor loop on a microcontroller and leave the Linux board for perception and planning. When the deadline-critical part lives on its own chip, the messaging layer stops being on the critical path. Whether your robot should run one computer or several is worth settling first.

How long has the project got before it has to work in front of someone?

With a demo inside a month, do not touch the foundation. That is a decision, not an evasion. Reduce what the busiest stream carries, close the tools nobody needs during the run, and cut scope. Each of those is reversible in an afternoon and none can break the demo the night before.

With a quarter, you can afford to prove where the time goes and try the cheap structural fixes: swap what sits under the existing stack, merge two programs that always live and die together, move one heavy consumer off the hot path. Most teams find something surprising in the first fortnight, and the surprise is usually not the transport.

With a year or more, decide by where the robot is going rather than where it is now. A product that will grow more sensors, more consumers of one stream and tighter timing is a product whose foundation is under steadily increasing load, and that is the case where changing early costs less than changing later. Why projects stall after the prototype is worth reading first.

What does your team already know how to do?

Your team's existing knowledge is worth more than any property of the stacks being compared, and teams routinely discount it to nothing. A group fluent in one middleware debugs a strange failure in an afternoon, because someone recognises the shape of it. The same group on an unfamiliar foundation spends a week on the same failure, because recognition is precisely the thing being rebuilt.

Language matters more than most comparisons admit. A team that writes Python and has never had to reason about who owns a piece of memory will find that a shared-memory foundation asks a question nobody has asked them before. A team already writing C++ or Rust finds the concepts familiar, and the honest cost becomes configuration rather than education. Whether to mix languages in a robot project covers the version that bites mixed teams hardest.

Hiring outlasts the decision. A widely known foundation means candidates arrive already speaking the language. A less common one means you train everyone, which is affordable while the team is small and becomes a permanent tax once it is not.

What do you give up by committing to one middleware early?

You give up optionality on everything that foundation is bad at, and you rarely know in advance which thing that will be. Commit to a large ecosystem and you inherit its assumptions about networking, its release cadence, and its end-of-life schedule, which becomes your upgrade schedule whether or not the timing suits your product. What happens when a distro goes end of life is the concrete version of that cost.

Commit to a smaller one and you give up the crowd. When your question is unusual, fewer people have answered it before you, the search results run out sooner, and the assistant you ask has read less material about your problem. That is a real and daily cost even when the technical choice was sound.

You also give up a certain honesty about ownership. Every foundation is maintained by people, and the pace and funding behind those people becomes a property of your product whether you examined it or not. Telling a healthy project from a dead one takes an hour and is skipped almost universally.

When is ROS 2 the better choice?

ROS 2 is the better choice for most robots being built today, and HORUS is the wrong answer for a project whose real need is everything except messaging. If you want mapping, autonomous navigation, arm motion planning, a way to see what the robot believes about the world, or drivers for hardware you have already bought, use ROS 2 and use the packages. They represent an enormous amount of accumulated work that no sensible team reproduces.

Use ROS 2 when your robot spans more than one machine, because a network-shaped foundation is the right shape for a network-shaped robot. Use ROS 2 when you are hiring, because candidates arrive already fluent in the vocabulary. Use ROS 2 when your team is large enough that a common standard beats any individual technical win, which happens sooner than founders expect.

And use ROS 2 when you have not yet proven where your time is going. Changing a foundation on a hunch is how teams spend a quarter and arrive at the same symptom with different tools. The fuller comparison of the two walks through where each one wins.

Can you keep your options open by hiding the middleware behind your own interface?

No, and here is why: the abstraction that would protect you has to be written before you know what you are protecting yourself from, and it leaks at exactly the moments that matter. Teams write a thin layer so no application code names the middleware directly, and it holds beautifully for simple values at a comfortable rate. Then the real requirements arrive. One stream has to survive a subscriber that stopped reading. Another has to be there before the next control cycle needs it. A third has to land in a format an external tool understands.

Each of those is expressed differently on each foundation, so the abstraction either grows a special case for every one or shrinks to the lowest common denominator of both, which surrenders the reason you were considering the change.

The version that works is narrower and much less satisfying. Draw the boundary around your algorithms rather than around the messaging. Keep the controller, the planner and the perception logic as ordinary code that takes inputs and returns outputs. Algorithms port. Plumbing does not.

Does changing middleware mean throwing away the robot software you have?

Partly, but not the way you think: what you lose is not the thinking, it is the wiring. The control law somebody tuned, the state machine handling the awkward cases, the calibration procedure worked out over a fortnight, the geometry that turns what the camera sees into where the arm should go, all of that is ordinary code and survives a foundation change nearly intact.

What does not survive is everything describing how programs meet. Message definitions, launch and configuration, discovery settings, the recording format, the parameter conventions, the harness that starts a fake robot. That is a great deal of unglamorous work, and it is where migrations actually spend their time.

There is a third category people forget: the tacit knowledge encoded in settings nobody can explain. Every mature robot carries values arrived at through suffering, and a migration reveals how many of them nobody understands any more. What migrating actually costs and what you keep itemises this properly.

How do you decide whether reversing is worth it?

Decide by writing down what the current foundation is blocking, in one sentence, with a product requirement in it. If the sentence says a specific requirement cannot be met on the current stack, and you have shown that with an experiment rather than an argument, a change may be worth paying for. If the sentence says the team finds the stack unpleasant, that is real and it is not a reason, because a migration has never once fixed a morale problem.

Then price the three things people always leave out: the drivers you did not write and would have to replace, the operational knowledge living in configuration nobody remembers writing, and the months during which every engineer is slower because everyone is a beginner again.

Then look for the smaller move. Almost every migration that succeeded began as a boundary rather than a rewrite: one new subsystem built the new way, running beside the old one, proving the case on something that could fail without taking the product down. When to rewrite and when to live with it keeps this honest.

Here is the whole decision in five lines.

The HORUS Fit Framework compresses this onto five axes you can score any option against: ecosystem size, setup effort, team size fit, deployment target, and licence. In a reversibility argument, licence and ecosystem size do most of the work, because those are the two you cannot quietly renegotiate later.

The cheapest moment to have an alternative in mind is before you need one. Put HORUS on that shelf now: star it so it is in your list when you start building.

Found this useful? Share it:Discuss on HNShare on X