To design better software, first establish some boundaries
How EBI architecture can help you build a change-friendly codebase
By Humberto Politi de Oliveira and Lucas Borges Moreira de Souza
Most software architectures don’t fail dramatically. They just slowly turn stale.
At first everything ships quickly. Time passes and rules start showing up in odd places with small differences. A validation here, a special case there. A new hire asks where the “real” logic lives and nobody gives quite the same answer. Changes still get made but swapping a vendor or adjusting a policy suddenly starts to feel risky.
As our re-commerce platform matured and our team scaled rapidly, we deliberately stepped back to assess whether our architecture was still serving the way we wanted to build. We had many capable engineers, from different backgrounds, all contributing at pace. That diversity was a strength, but without clearer structural boundaries, business rules were gradually spreading across controllers, models, background jobs and helpers. Tests grew harder to reason about, documentation drifted, and each change required more care.
Rather than reaching for a rewrite, we chose a more constrained and sustainable response: tightening the rules around where different kinds of code are allowed to live. The architecture we settled on was Entity–Boundary–Interactor, or EBI. It’s not new or glamorous. It just works.
The real problem wasn’t technical
When we stepped back, the most obvious issue affecting our architecture wasn’t performance or tooling. At its most basic it was a disconnect — subtle but significant — in the language used by different disciplines and different engineers.
Product managers talked in terms of use cases: approve a refund, publish a product, apply a promotion code. But engineers talked about frameworks, models, controllers, services and jobs. And in the end the same rule might reasonably appear in three places, each correct in isolation but wrong in combination.
We didn’t need a smarter database or a faster queue. We needed a common language and a shared map of the system to make it obvious where a rule does and doesn’t belong.
EBI in a nutshell
EBI splits code into three roles, with strict one-way dependencies:
- Entities hold business rules and invariants. They describe what is allowed, in plain domain language, without knowing anything about databases, APIs or frameworks.
- Boundaries are named contracts with the outside world: persistence, payment providers, notifications, queues, analytics and so on.
- Interactors (aka use cases) describe the steps of a specific action: take input, apply rules, decide outcomes, ask for outside work to be done.
Interactors orchestrate entities and their actions, compile results and call boundaries. Entities never talk to the outside world. Boundaries never contain business rules. That’s the whole rule set.
If this sounds familiar, it should. EBI is closely related to Clean Architecture and hexagonal design. What appealed to us was how small and teachable the vocabulary is — and how opinionated it is about where things go.
A (more or less) real-world example
Suppose you have a PublishArticle feature. The input contains a title, article body and reviewer IDs.
The interactor builds an Article entity. The entity enforces rules: title required, length limits and allowed state transitions (draft, reviewed, published). The interactor persists the entity via a repository boundary and calls a notifier boundary to alert subscribers. Any output — success, validation errors or “needs review” — is returned in a simple, explicit form.
Nothing in that flow knows whether you’re using Rails, Django, a message queue or a Cron job. Those details live behind boundaries. The business logic doesn’t move when infrastructure does.
What it has changed
EBI isn’t a diagram to tape on a wall; it’s a set of constraints that make software kinder to change. By separating business rules from frameworks and wiring, it gives your system a spine — entities model what the business cares about, interactors (use cases) orchestrate the rules, boundaries declare contracts.
Here are some ways it has improved things for us.
Consistent code quality
Business rules stopped drifting. Each invariant has one home, inside an entity. We no longer re-implement “just in case” checks in controllers or ORMs. When a policy changes there’s a single, obvious edit.
Interactors make flow explicit. You can read a use case from top to bottom and see how a decision is made. There’s no spelunking through callbacks or framework hooks to understand why something happened.
Boundaries force better naming. Calling something PaymentGatewayInterface or UserRepositoryInterface might sound trivial but it exposes accidental coupling fast. During reviews it becomes harder to sneak in “just one SDK call” without acknowledging it as a dependency.
Because dependencies only point inward, we can enforce the architecture mechanically. Simple build-time checks prevent outward layers from incorrectly importing inward ones. The structure resists entropy without just relying on everyone remembering the rules.
Error handling has improved too. Interactors translate infrastructure failures into domain outcomes understood by the rest of the system, such as “try again later” or “needs review”.
Faster development
Because product managers and developers now meet in the same place — the use case — we can immediately refer to the same flow in the code and talk about the same steps in plain language. EBI has reduced the need for debating and searching.
There’s a repeatable recipe for new features: define inputs and outputs, sketch boundaries, implement the interactor, then wire concrete implementations. The result is less time spent figuring out where code goes, and more time available for writing it.
The consistent layout helps everyone. Engineers know where entities, use cases and boundaries live. That alone cuts down navigation time and avoids the creation of yet another vague “Utils” folder under deadline pressure.
It’s easier to work in parallel too. Because everything depends on contracts we can mock up boundaries easily. We can deliver the core use case first, running entirely in memory, while infrastructure is still in progress: one person adjusts an entity, another updates an interactor, a third wires a gateway. We experience fewer merge conflicts and “blocked on infra” updates.
Better testability
When business logic lives in interactors and entities, unit tests run fast. There’s no need to boot frameworks or hit networks; use cases run in memory. Tests become faster and more precise.
Serialization boundaries become explicit. Data Transfer Objects (DTOs) and presenters mark where data crosses layers, making transformations easy to test in isolation. We rely less on brittle end-to-end tests that fail for reasons unrelated to the behavior we’re actually interested in.
Continuous Integration (CI) pipelines benefit too. Fast tests run on every change. Slower integration checks focus only on boundaries that touch external systems. The signal-to-noise ratio improves.
Easier onboarding and reviews
New engineers now start with entities to learn the domain language. Then they read interactors to see how those rules compose. They learn the system by following intent, not by deciphering framework choreography.
Code reviews improve, too. Pull requests naturally split into smaller scopes — entity changes, use case changes, boundary changes or framework wiring. That clarity makes feedback sharper and more actionable.
The predictability also plays well with tooling. Code generators and AI assistants spin up boundaries, fake implementations and tests without polluting the domain layer. Automation becomes an ally rather than a source of accidental coupling.
The trade-offs are real
EBI isn’t free. The costs are real, but they’re also front-loaded and manageable.
The first slice of a feature is heavier. You often define interfaces, DTOs and an interactor before shipping anything visible. If you’re used to framework-first development this can feel slow, but the payoff is that you avoid time-consuming rewrites later as requirements take shape.
EBI tolerates duplication, on purpose. Two domains may both talk about “Users” differently, and that’s fine — sharing entities too early creates coupling that’s painful to undo. Translation at the edges, via DTOs or mappers, costs a bit more code but preserves autonomy.
There’s a learning curve, of course, though it’s gentler than it looks. The vocabulary, the discipline of keeping interactions one-way, and habits like testing use cases in memory all differ from framework-first development practices.
Teams ramp faster after a few deliberate practice sessions. Pairing on the first few interactors and keeping a small glossary are helpful practices. The goal isn’t ceremony, it’s to develop shared mental models quickly.
Crucially, this was not a big-bang rewrite. We applied EBI to new work and to older areas that changed often. The benefits accumulated without stopping delivery.
Why this scales with teams, not just code
The biggest win wasn’t technical elegance, it was alignment.
Product managers and engineers now talk about the same things in the same place: use cases. When someone says, “Let’s change the refund flow,” everyone opens the same file. A policy tweak touches one entity. A process tweak touches one interactor. A vendor swap touches one boundary.
This shared structure makes changes easier to predict, easier to review and easier to trust. Incidents become easier to diagnose. Automation becomes more effective. The codebase feels calmer.
The takeaway
EBI isn’t a new tool to buy. It’s a simple, reassuringly dull set of constraints that make change safer.
You pay a small cost up front in ceremony and interfaces. In return you get clearer code, faster tests, steadier releases and a system that scales with the team working on it. Business rules have a single, framework-free home. Dependencies become intentional. Architecture stops being a diagram and starts being an everyday guardrail.
We didn’t adopt EBI all at once, and we’re still not finished. It began with new work, where introducing structure was cheapest and least disruptive. As those patterns proved useful, we started applying them to existing parts of the system, not through rewrites but incrementally, as those components changed. New features became the natural point to introduce interactors, move rules into entities and push infrastructure behind boundaries. We’re still partway through that transition, but even at this stage it has proved to be a very useful tool for making change safer and the codebase easier to reason about.
If your codebase feels like a maze, don’t rewrite it. Pick one new module. Treat entities as the source of truth, boundaries as contracts and interactors as conductors. The payoff isn’t abstract. It’s the moment a new engineer opens the repository and immediately knows where the code is — or at least, where it should be.
The authors are Lead Software Engineers at MPB, the largest global platform to buy, sell and trade used camera gear.

