I joined R2 in 2021, the same year the company was founded. At the time, we had a single partner, a payment processor in Mexico and a platform designed entirely around that one relationship. Early on, we built our system around a single market, with one integration and a set of rules designed for that specific context. That's a natural place to start. But as R2 expanded into new countries and new partner types, some of those early decisions became harder to sustain. The real challenge is knowing which ones still serve the business and which ones need to evolve.
Five years later, R2 processes transactions for millions of merchants across Mexico, Colombia, Peru, Chile, and Brazil. We run underwriting models, manage collections, integrate with local payment rails, and maintain reliability levels that our partners' own systems depend on. The engineering decisions we made early on, good and bad, are what made that possible, or what made it harder than it needed to be.
The decision I’d make again is to avoid premature optimization while building a simple, extensible architecture from the beginning. We wanted to add complexity as the business needed it, with a foundation that left us room to change. That included changing core architectural decisions without turning every change into an enormous engineering effort.
In practice, that meant being disciplined about ports and adapters. A port defines a common contract: what a component needs to do and how the rest of the system interacts with it. An adapter connects that contract to a particular provider or integration, handling the details specific to it. That separation lets us reuse components while giving country-specific integrations a clear place to live. We could define those boundaries without building every possible integration in advance.
Payments are a concrete example. We work with different payment and disbursement providers across countries, but use one common contract for sending and receiving payments. When we integrate a provider, we add or adjust its adapter to connect it to that contract. The provider-specific details belong in that adapter; they do not require us to change the base payment port. We still have integration work to do, but we have an existing structure to extend instead of rebuilding that part of the system for each country.
As R2 has expanded, the architecture has evolved and supported the business’s needs at each stage. New markets and business opportunities have given us concrete requirements to address. Those reusable components and clear integration boundaries have let us build on what already works without rebuilding the system almost from scratch.
The decision I'd take back is one we made for the right reasons at the wrong moment: we distributed our architecture too early.
When we started building, the industry had already been moving toward microservices for some time, breaking systems into many small, independent components, each one handling a specific job. At the time, that approach felt modern and scalable, so we followed it. The problem is that we were a team of four engineers managing a system split into 30 to 40 separate components spread across multiple code repositories.
Every new feature required touching several repositories simultaneously. We were still building the company, and much of the foundational code we have today did not yet exist. Switching between repositories meant repeatedly changing context, which took time away from building features and, in some cases, increased their time to market. We had built a system with the operational complexity of a large organization, while still being a team small enough to fit in one room.
What we should have started with and what we eventually rebuilt toward is a modular monolith: a single, unified codebase with clear internal divisions between different areas of responsibility. Each module handles its own domain cleanly, but they all live together. You get the organizational clarity of separation without the overhead of managing dozens of independent systems. When you're a small team, that overhead is not a theoretical concern. It's a daily tax on everything you try to build.
We learned that lesson the hard way. But it has shaped how we think about complexity ever since.
A common misconception about Latin America is that it functions as a single, coherent market. From an engineering standpoint, that assumption breaks quickly. Every country we've entered has its own financial regulations, its own data standards, and its own definition of what constitutes a valid transaction. What works in one market needs to be rethought, sometimes rebuilt, before it works in the next.
The architecture we've deployed to handle that diversity rests on three principles. None of them is optional.
Underneath all of that sits a challenge that doesn't get enough attention: data consistency. The information that flows from our partners into our systems has to be exactly right, not approximately right. Events we send to partners need to be confirmed as delivered. We build features with self-verification built in, if something can be checked automatically within the flow, we check it. At the scale we operate, any drift in data integrity compounds quickly. Fixing it manually after the fact is a sign that the system wasn't designed well enough.
When you're building a product that moves money, quality means something different. In a typical app, a bug might mean a screen doesn't load. In a financial product, it can mean a payment goes through when it shouldn't, or fails when it should. That's why we've always paid close attention to how different parts of the system work together, especially when things don't go as planned.
Before an integration goes live, we test more than just the expected scenarios. We look at what happens when processes fail, when systems don't respond as expected, and when a transaction moves through different states. We also make sure both sides agree on what's happening before any money moves. These checks happen behind the scenes, but they're a big part of what makes a financial product reliable. Our partners need to know that things will work as expected, even when something goes wrong.
Every partner has its own systems, data and ways of working. Connecting them to R2 means making sure their systems can communicate with ours without changing the financial infrastructure that powers our products. But an integration is about more than getting two systems to talk to each other. It also means making sure the partner's product can rely on R2 in real time. Take a ride-hailing platform: every time a user with financing takes a trip, the partner's system needs to communicate with R2's APIs. If our response is slow, the impact is felt directly by the user. That's why we agree on service levels with each partner before launch and continuously work to improve performance, down to the milliseconds that matter most. Some of the key metrics we track and commit to as part of our SLAs include:
We build checks into these flows to confirm that events are delivered correctly and that both sides have consistent information. This helps us identify and resolve discrepancies as they happen, and this is part of what makes an integration reliable over time: not just getting it live, but making sure it keeps working as the partner's business grows.
At R2, the value an engineer brings starts with understanding the problem, deciding what to build, and taking responsibility for the outcome. Writing working code is part of that responsibility. The decisions around it matter just as much: whether we are solving the right problem and whether the solution works for the partner once it reaches production.
That takes technical judgment. We need to understand the system well enough to anticipate where a change might have unintended effects, break a large problem into pieces small enough to ship safely, and recognize when the right answer is not to build something at all. Knowing a programming language gives us the means to implement a decision. Experience operating systems helps us judge its consequences.
At R2, one engineer typically owns the full lifecycle: talking with the partner to understand the problem, designing the solution, building and testing it, deploying it, and operating it once it is running. The person making implementation decisions stays connected to the problem that prompted the work and to what happens after deployment. Ownership includes following through when the system behaves differently from what we expected.
That responsibility asks us to think beyond the component we are changing. A local change can affect another part of the flow, and a successful deployment does not by itself tell us whether we solved the original problem. We need to connect the partner’s needs with the design, the behavior we test, and what we learn from operating the result.
When someone joins the engineering team at R2, learning the codebase is part of learning that full flow. We also need to understand why each piece was designed the way it was, how a financing offer moves from request to disbursement, and what happens at each step when something goes wrong. Documentation gives us the design decisions and context to work from. Working closely with the system over time builds on that knowledge, showing how those decisions hold up in practice and where our understanding needs to grow.
AI has changed how we work at R2, but adoption has been uneven. Some engineers work fluently with agents; others find it harder to make them useful in their day-to-day work. We cannot treat access to the same tools as evidence that everyone is getting the same benefit. Learning how to provide context and evaluate the output is part of the work.
Our repositories now need to make sense to both people and AI agents. That means making the system’s boundaries, conventions, and verification instructions clear enough for either to follow. An agent needs context about what a component does, where it fits, and what constraints a change must respect. The relevant information needs to be available in the repository rather than assumed to exist in someone’s head.
In our experience, the bottleneck has moved from writing code to verifying it. Producing a plausible implementation is only one step. We still need to understand its behavior, check that it fits the surrounding system, and examine the consequences of putting it into production. More code arriving faster makes that work more pressing.
AI can help with development and analysis, including the work of verification. It can help us explore an implementation or reason through a failure. But we remain responsible for deciding what evidence is sufficient and whether a change is ready to ship. A convincing explanation from an agent is not proof that the system behaves as intended.
One architectural boundary has stayed the same: we have always chosen deterministic code and technology for critical parts of R2, and we continue to do so. Those parts must not depend directly on an LLM to operate at runtime. Model outputs are probabilistic; the rules governing a critical financial operation need to be explicit and verifiable.
Using AI to help design, write, or test that code is a separate decision from putting a model in its runtime path. My expectation for the next few years isn’t that engineers are replaced. It’s that the balance of their work shifts toward deciding what to build, what not to build, and how to verify the result. Structuring work so that people and AI agents can execute it reliably becomes a larger part of the role. Engineers still need to understand the system well enough to make those calls and take responsibility for what they deploy.