most read
Software Engineering
Why We Killed Our End-to-End Test Suite Sep 24
Product
Product Managers: what they do and why we need them Feb 15
Software Engineering
The value of canonicity Oct 30
Careers
We are building diverse teams with the most creative and innovative professionals for each position we open.



Written by: Breno Oliveira
When a developer needs to modify a critical service, the requested change appears straightforward, but understanding its consequences is another matter.
The service has accumulated years of business logic. In this context, some dependencies are poorly documented, certain behaviors are difficult to reproduce locally, and the available tests offer only partial reassurance. Because of that, a modification that looks isolated could affect an entirely different part of the system.
The developer, then, has two options: investigate further, accepting an uncertain delivery timeline, or implement the change and hope the existing safeguards catch any unintended consequences.That said, neither option is particularly appealing.
What makes this situation interesting is that the problem might have nothing to do with the team’s culture. The engineer could work alongside supportive colleagues, feel comfortable asking questions, and have complete freedom to propose improvements. Yet the code itself remains difficult to trust.
This distinction reveals an often-overlooked dimension of developer experience. Engineering confidence depends not only on relationships between people but also on the degree to which software systems can be understood, tested, and safely modified.
And as systems grow, that confidence becomes increasingly connected to the architectural decisions made along the way.
The uncertainty behind a seemingly simple change
Software complexity is not inherently a problem. A system that handles thousands of business rules, integrates with external services, or processes critical financial transactions will naturally involve complexity.
The difficulty begins when engineers cannot determine where that complexity resides or how different parts of the system influence one another.
Consider a service responsible for several business capabilities. A developer modifying one of those capabilities may need to understand unrelated workflows because they share internal state, dependencies, or assumptions.
Even when the change itself requires only a few lines of code, assessing its impact can become a substantial investigation. This is where technical uncertainty begins to influence engineering behavior.
An engineer who cannot confidently identify the consequences of a modification may postpone a refactor, introduce an additional abstraction to avoid touching existing logic, or request extensive review from someone more familiar with the system.
These decisions can be entirely rational. In a production environment, caution is often preferable to introducing unnecessary risk. However, when this pattern becomes routine, an organization can gradually lose the ability to evolve its software efficiently.
Delivery estimates begin to reflect not only implementation effort but also the time required to investigate unknown dependencies. Onboarding becomes more demanding because understanding the system depends on knowledge that is rarely written down. Senior engineers may become recurring points of consultation, limiting their availability for other technical challenges.
The underlying issue is not necessarily that engineers lack autonomy. It is that exercising that autonomy carries too much uncertainty.
Check our job opportunies
Architecture determines how much an engineer needs to know
One of the most consequential architectural decisions is where to establish boundaries between responsibilities. A well-defined boundary allows engineers to reason about a component through its documented behavior and interfaces without understanding every detail of its implementation.
For example, a team working on a card-related product should ideally be able to consume common card capabilities without having to understand the business rules governing every other product that uses them, but achieving this separation is rarely straightforward.
Capabilities that appear independent from a product perspective may share data, operational constraints, or historical assumptions. Extracting them into reusable components requires understanding those relationships and deciding which responsibilities genuinely belong together.
Still, the benefit extends beyond code organization. When architectural boundaries correspond to meaningful responsibilities, engineers can narrow the scope of their investigations. They can identify which components are affected by a change, which contracts must remain stable, and which tests should provide evidence that the modification behaves as expected.
This makes uncertainty easier to locate and manage, and the distinction becomes particularly important when an architecture originally designed around a small number of products must accommodate an expanding business.
What Nubank’s architectural evolution reveals about change
Nubank’s engineering history provides a useful example of how architectural requirements evolve alongside an organization. As described in the article Evolving Nubank’s Architecture Toward Thoughtful Platformization, the company’s original architecture was designed to support a single product, a credit card, in a single market, Brazil.
That architecture reflected the business requirements of its time, but as Nubank expanded into additional financial services and countries, however, the same architectural assumptions could no longer accommodate every new requirement as naturally.
Different products needed access to common capabilities, while individual markets introduced their own business rules and regulatory considerations. The challenge was to distinguish what should be shared from what needed to remain specific to a product or geography.
One example discussed by Principal Engineer Bel Gonçalves during Nubank Engineering Meetup #12 was the evolution of the card platform. Previously, functionality was concentrated in services such as CreditCardAccounts, whose responsibilities were closely associated with credit cards.
As the business expanded, Nubank evolved toward more flexible services capable of supporting debit and credit functionality across different products and markets. The published account highlights two distinct contexts: Brazil’s combination cards and Mexico’s separate debit and credit cards.
Supporting these different configurations required an architecture capable of separating reusable card capabilities from the rules governing particular products and countries. This is where platformization becomes relevant to the broader question of engineering confidence.
An architecture organized primarily around a specific product may require engineers to navigate product-specific assumptions when adapting functionality to a different context. Separating reusable capabilities from localized logic can make those assumptions more explicit.
In principle, this allows teams to reason about shared behavior independently from the business rules of individual products. The significance is not simply that services become reusable. It is that their responsibilities can become easier to identify. For developers, that clarity provides a stronger foundation for understanding the potential impact of a change.
Nubank’s published account does not establish that engineers working on CreditCardAccounts lacked confidence or that the transformation measurably improved their experience. The documented achievement is architectural: a shift toward services with greater flexibility across products and markets.
Nevertheless, the case illustrates an important engineering principle. As responsibilities become more clearly separated, the amount of unrelated system behavior that developers must understand to make a particular change can potentially decrease.
The trade-offs behind safer architectural evolution
Clearer boundaries are valuable, but creating them can introduce risks of its own. Nubank’s platformization involved extracting critical functionality from legacy services already running in production, as well as rewriting foundational components. These activities require more than deciding where code should live.
Moving functionality between services can affect data ownership, consistency guarantees, dependencies, deployment coordination, and failure handling. An abstraction that simplifies one team’s work might introduce additional complexity for another.
There is also the risk of generalizing too early. A platform designed to accommodate every conceivable future product can become difficult to understand precisely because it exposes too many configuration options and extension points. In that situation, flexibility becomes another source of uncertainty.
The June 2025 account explicitly identifies the need to balance generality with simplicity and avoid unnecessary engineering complexity. That balance matters because maintainability cannot be measured by the number of services created or the amount of code extracted.
A useful architectural boundary should make responsibilities clearer without forcing developers to navigate an unnecessarily complicated network of abstractions.
Sometimes, keeping functionality together is the simpler and safer choice. At other times, separating it creates the conditions for independent evolution. The engineering challenge is knowing which trade-off the system actually requires.
Confidence comes from evidence, not assumptions
Architecture determines much of what engineers need to understand before making a change. But understanding the structure of a system is only part of the problem.
Engineers also need ways to verify their assumptions. Automated tests are particularly valuable when they describe meaningful behavior rather than merely exercising implementation details.
A test that establishes how a service responds to a specific business condition can serve two purposes. It checks whether the software behaves correctly, and it communicates what future modifications must preserve. This becomes especially important when changing foundational components that support several products.
However, tests cannot provide unlimited reassurance. A passing suite says little about scenarios it does not cover, and tests coupled too closely to implementation details can make legitimate refactoring unnecessarily difficult.
Confidence depends on understanding both what the tests verify and where their coverage ends. Debugging and observability provide another layer of evidence. Logs, metrics, traces, and reproducible development environments can help engineers understand how a system behaves beyond what its source code suggests.
Interactive development tools can also shorten the distance between a hypothesis and its verification. In Clojure, for instance, the REPL allows developers to evaluate expressions and inspect behavior during development.
These capabilities are complementary rather than interchangeable. A REPL can help explore a function’s behavior, while distributed tracing may be necessary to understand how a request moves between production services.
Documentation addresses a different problem: the loss of architectural context over time. An interface might explain how to interact with a component without revealing why it was designed that way. Architecture Decision Records (ADRs) can preserve the reasoning behind consequential decisions, including the alternatives considered and constraints that influenced the outcome.
Such records are particularly useful when future engineers encounter a design that seems unnecessarily restrictive until its historical context becomes clear. Shared technical standards can further reduce the effort required to move between systems.
The same Building Nubank article describes the company’s use of Clojure, microservices, and asynchronous communication as a common engineering foundation. It also discusses immutability as an important design principle.
Consistency does not guarantee maintainability, but familiar patterns can reduce the number of conventions engineers must learn when working across services. Together, these practices make changes more explainable and their consequences more observable. They cannot remove operational risk, but they can help teams make decisions with better evidence.
The human consequences of architectural decisions
There is an important distinction between feeling safe with colleagues and feeling confident about modifying a system. Psychological safety, in its established organizational meaning, concerns whether people feel able to speak up, acknowledge mistakes, ask questions, and take interpersonal risks without fear of humiliation or punishment.
Technical confidence concerns something different: whether engineers have sufficient understanding, tools, and evidence to make changes with an acceptable level of risk.
A team can have one without the other. An engineer might freely acknowledge uncertainty during a code review while remaining unable to determine whether a modification will affect an unrelated service. Conversely, an engineer might understand a codebase thoroughly but work in an environment where questioning decisions is discouraged. Both conditions matter, and neither substitutes for the other.
The expression code psychological safety, explored by Breno Salgado, offers a useful metaphor for the relationship between developers and the systems they maintain. Its value lies in drawing attention to the human consequences of technical design, while preserving the distinction from interpersonal psychological safety. Those consequences extend beyond individual comfort.
When system behavior is difficult to understand, knowledge can become concentrated among a small group of experienced engineers. Their involvement may become necessary for routine changes, creating dependencies that affect both delivery and professional development.
New engineers may spend more time learning undocumented exceptions than developing a broader understanding of the product. Experienced engineers may devote disproportionate attention to explaining historical behavior instead of addressing emerging problems.
A maintainable system cannot remove the need for collaboration, nor should it. Complex engineering work depends on shared expertise, but good architecture can help make collaboration a choice driven by the problem rather than a recurring requirement imposed by opaque code.
The real measure of maintainability
A codebase can be stable in production and still be unnecessarily difficult to change. That distinction matters because operational stability describes how a system behaves today, while maintainability influences how effectively it can evolve tomorrow.
Nubank’s architectural evolution illustrates the importance of revisiting design decisions as business requirements change. An architecture that serves a single product well may need different boundaries when it begins supporting multiple products, countries, and regulatory contexts.
The goal is to make complexity understandable enough that engineers can work with it deliberately. That means designing boundaries around meaningful responsibilities, preserving the reasoning behind decisions, investing in reliable feedback mechanisms, and recognizing when an abstraction creates more uncertainty than it removes.
Ultimately, an engineering organization should not have to choose between protecting its existing systems and giving developers room to improve them. The most maintainable architecture is not necessarily the one that changes most easily. It is the one that helps engineers understand what a change will affect, verify their assumptions, and manage the risks that remain.
At the end of the day, confidence in software engineering comes from having enough knowledge and evidence to move forward responsibly, even when something might go wrong.
Check our job opportunies