Simplicity’s Irony: When inaccurate modeling creates needless complexity
As you can tell, I'm obsessed with simplicity. Most articles I write talk about the problems of both over-engineering or under-engineering. During my whole career, however, I've stumbled upon more cases of under-engineering and oversimplification than the other way around. The question remains open: What's simple? And I've got to be honest. I don't know. A swarm of ideas compete in my head so instead today I'll let some of my experiences take the spotlight and grow into a lesson I hope other developers and engineers can synthesize. Undertrail Back in 2015 I started working for a company called Undertrail (later acquired by Hopper). The company was growing at a very scary and promising pace. They attempted to become the GDS (Global Distribution System) for lowcost airlines (and other transport means) in Latam. Undertrail's solution was a Frankenstein's monster composed of bad decisions, messy workarounds, and unfortunate oversimplifications. It was a miracle its software even started. It was also no surprise that it was crashing more than once a day. I was their first in-house developer. Their solution was built by a freelancer. I was handed over that solution, and I was expected to take care of it as the company grew. To this day I thank Undertrail for the opportunity to work for them, and for the mess they had to work with, because otherwise I wouldn't have known how enjoyable is to solve those problems. Providers and airlines are the same thing, ain't them? This was the first real problem I faced here. When trying to integrate a big provider of flight tickets (one whose name I don't remember but which is currently known as kiwi.com), I noticed a big blocker due to the fact that the freelancer guy had modeled ticket providers and airlines as the same thing. Including kiwi.com wasn't as straight forward as it should be due to the fact that it provided tickets for multiple airlines, some of them, already hardcoded into our solution. The solution involved lots of workarounds, but all of them going in the same direction: Rebuilding the understanding behind our code—Airlines and Ticket providers are two different concepts. The simple booking Previous fix indirectly solved scalability issues, without the need to add more CPU or memory resources in the pot. After that Undertrail identified an opportunity for horizontal growth. This time, the focus shifted to giving customers finer control over what are typically called additional services: extra legroom, unusually shaped luggage, inflight meals, and similar offerings. Then came the first snag: payments were tightly coupled with bookings (same table/collection, 1-to-1 relationship, same lifecycle)—a legacy domain quirk that refused to die. Rather than rethink the flawed model, developers patched around it. The result? Separate payment flows for bookings and add-ons… plus a Frankenstein edge case for combined payments. Thankfully, my proposal was adopted before extensive workarounds were implemented. By decoupling Payments from the Bookings, we ensured Payments had their own lifecycle, and could be seamlessly tied to any future products, and current ones. The obvious Purchase Engine Undertrail had a team of what we called "The bookers". They were necessary to complete the lifecycle of the booking. Bookings were created, paid, processed and delivered. The processing part was completed by the bookers. It involved manually buying the purchased flights in some of our providers. Once we saw the opportunity to automate that part, developers mindlessly started providing solutions. The winning one: A new state in-between paid and processing. The result? Confused bookers, unclear lifecycle, double purchases ($$$), and more. The real solution: There's no new state. We just have a booker that instead of using the UI, uses the API, but does exactly the same: Gets quotes, picks the best, and buys the products. A new different problem, that didn't necessarily change how we processed bookings. Foodology Back in 2021 I was begging to the universe for the chance to work at a company that was growing real fast, yet with a solution that couldn't handle that growth. Foodology was that company. They operate hidden kitchens all over Latam, and their main source and only source of revenue, comes from selling food via food delivery platforms like UberEats, DoorDash and Rappi. Their solution was also a Frankenstein's monster. A monolith tailored to receive orders from our main provider (Rappi), and painfully patched to support other food delivery providers that came later. So we sell via Rappi, right? When I arrived, their solution clearly suffered from a oversimplified understanding of their business domain. Instead of seeing themselves as a company that sells food through multiple heterogeneous means, they saw themselves as a company that sold food through Rappi. The initial solution to that mess was t

As you can tell, I'm obsessed with simplicity. Most articles I write talk about the problems of both over-engineering or under-engineering.
During my whole career, however, I've stumbled upon more cases of under-engineering and oversimplification than the other way around.
The question remains open: What's simple? And I've got to be honest. I don't know. A swarm of ideas compete in my head so instead today I'll let some of my experiences take the spotlight and grow into a lesson I hope other developers and engineers can synthesize.
Undertrail
Back in 2015 I started working for a company called Undertrail (later acquired by Hopper).
The company was growing at a very scary and promising pace. They attempted to become the GDS (Global Distribution System) for lowcost airlines (and other transport means) in Latam.
Undertrail's solution was a Frankenstein's monster composed of bad decisions, messy workarounds, and unfortunate oversimplifications. It was a miracle its software even started. It was also no surprise that it was crashing more than once a day.
I was their first in-house developer. Their solution was built by a freelancer. I was handed over that solution, and I was expected to take care of it as the company grew.
To this day I thank Undertrail for the opportunity to work for them, and for the mess they had to work with, because otherwise I wouldn't have known how enjoyable is to solve those problems.
Providers and airlines are the same thing, ain't them?
This was the first real problem I faced here. When trying to integrate a big provider of flight tickets (one whose name I don't remember but which is currently known as kiwi.com), I noticed a big blocker due to the fact that the freelancer guy had modeled ticket providers and airlines as the same thing.
Including kiwi.com wasn't as straight forward as it should be due to the fact that it provided tickets for multiple airlines, some of them, already hardcoded into our solution.
The solution involved lots of workarounds, but all of them going in the same direction: Rebuilding the understanding behind our code—Airlines and Ticket providers are two different concepts.
The simple booking
Previous fix indirectly solved scalability issues, without the need to add more CPU or memory resources in the pot. After that Undertrail identified an opportunity for horizontal growth. This time, the focus shifted to giving customers finer control over what are typically called additional services: extra legroom, unusually shaped luggage, inflight meals, and similar offerings.
Then came the first snag: payments were tightly coupled with bookings (same table/collection, 1-to-1 relationship, same lifecycle)—a legacy domain quirk that refused to die. Rather than rethink the flawed model, developers patched around it. The result? Separate payment flows for bookings and add-ons… plus a Frankenstein edge case for combined payments.
Thankfully, my proposal was adopted before extensive workarounds were implemented. By decoupling Payments from the Bookings, we ensured Payments had their own lifecycle, and could be seamlessly tied to any future products, and current ones.
The obvious Purchase Engine
Undertrail had a team of what we called "The bookers". They were necessary to complete the lifecycle of the booking. Bookings were created, paid, processed and delivered. The processing part was completed by the bookers. It involved manually buying the purchased flights in some of our providers.
Once we saw the opportunity to automate that part, developers mindlessly started providing solutions. The winning one: A new state in-between paid and processing. The result? Confused bookers, unclear lifecycle, double purchases ($$$), and more.
The real solution: There's no new state. We just have a booker that instead of using the UI, uses the API, but does exactly the same: Gets quotes, picks the best, and buys the products. A new different problem, that didn't necessarily change how we processed bookings.
Foodology
Back in 2021 I was begging to the universe for the chance to work at a company that was growing real fast, yet with a solution that couldn't handle that growth. Foodology was that company.
They operate hidden kitchens all over Latam, and their main source and only source of revenue, comes from selling food via food delivery platforms like UberEats, DoorDash and Rappi.
Their solution was also a Frankenstein's monster. A monolith tailored to receive orders from our main provider (Rappi), and painfully patched to support other food delivery providers that came later.
So we sell via Rappi, right?
When I arrived, their solution clearly suffered from a oversimplified understanding of their business domain. Instead of seeing themselves as a company that sells food through multiple heterogeneous means, they saw themselves as a company that sold food through Rappi.
The initial solution to that mess was to create an agnostic receiver of orders (affectionately called the Cookie Monster by the team), implicitly challenging the original misconception that there are no steps between receiving an order, and sending it to the kitchen. Then we built "integrators" for each one of the platforms we integrated, each one translating from their representation of an order, to our representation of an order. That enabled the team to quickly integrate more platforms without the need to modify the core receiver.
It's just menu management
At some point, when the team finally had some space to address some not so critical parts of the system, to reduce costs and improve efficiency, the team started working on automating the menu management.
You know that whenever you use one of these food delivery apps, you can explore the menu of each of the restaurants in the neighborhood, right?
Well, given the fact that Foodology had more than 100 kitchens, and in each kitchen we could sell about 10 different brands of food, and considering that we had already integrated about 10 different food delivery platforms. How many stores (and consequently menus) did we have to manage? 10k, approximately, right?
Well, the provided solution ended up being a webapp in which you uploaded an Excel file modeling the menu, and manually selected the stores in which you wanted to place that menu... simple.
Solution proved clunkier than anticipated. Implementation was very naive. You read the file, and then you synchronize the menu in all the selected stores.
But then, given each platform is different, how do you manage the fact that each platform has a different API? Do you have to upload the file again if you want to reuse it later? What about checking the current status of a store? Or what happens in the case of failure during synchronization?
When I turned my eye in that direction, I persuaded the team to re think the flow. When we came with a better solution, product people asked me "why so complex? Isn't it just menu management?"
Well, the proposed solution was composed of three components
- A menu manager: a simple crud for adding and modifying menus and menu drafts.
- A menu assigner service: menus and stores are different. How are they related to each other? This service is the one telling you which menu goes in which store. Simple as that.
- Multiple different menu synchronizers: Taking into account the fact that each platform worked differently. Instead of conflating everything into a single service, we created one per integration, that was in charge of translating our menu representation into their representation, and allowing us to more finely manage errors, retrials and versioning.
It's more complex, indeed, but ...
The conclusion
What can we observe from all these examples?
They all challenge a domain model and turn it into a more complex one. All of the examples seem to be an ode to complexity, but are they?
What resulted from all these wrongly simplified models was a rigidity that forced developers to build and evolve around it, adding more complexity that the one that inherently comes from the domain.
Oversimplification causes rigidity. Rigidity causes workarounds. Workarounds cause complexity. The irony of it.
The real world is littered with failed models—flat earth, geocentrism, luminiferous aether, caloric theory—each manageably simple yet catastrophically inaccurate. These frameworks clung to relevance only by grafting ever more exceptions onto their brittle cores, patching over mismatches between theory and reality.
A good developer doesn't just write simple code. A good developer is aware that coding is an act of modeling domains, and that every different domain comes with an amount of complexity that cannot be avoided. A good developer has a sharp and keen eye to see and foresee this complexity, and builds software that accurately models it, consequently avoiding the extra complexity caused by misinterpreting reality.
"Simplicity doesn’t come from ignoring complexity—it comes from accurately splitting it into, and rebuilding it from, manageable, composable parts."