What Hexagonal Architecture Adds to a Boundary-Control-Entity (BCE) Project 📎
In a recent project (eBank: accounting, customers, transactions, reporting) I checked what Hexagonal Architecture would add to an application organized with Boundary-Control-Entity (BCE). The answer: more classes and little else.
In BCE the unit of organization is the business component (BC), a package named after a responsibility. Each BC has up to three layers: the boundary adapts external protocols (JAX-RS, MCP), the control holds stateless business logic, the entity carries state and behavior. The same capability in both styles:
# BCE airhacks.ebank.accounting.boundary.AccountsResource airhacks.ebank.accounting.boundary.AccountsTool airhacks.ebank.accounting.control.AccountCreator airhacks.ebank.accounting.entity.Account # Hexagonal airhacks.ebank.adapters.in.web.AccountsController airhacks.ebank.adapters.in.mcp.AccountsTool airhacks.ebank.application.port.in.CreateAccountUseCase airhacks.ebank.application.port.out.SaveAccountPort airhacks.ebank.application.service.CreateAccountService airhacks.ebank.domain.Account airhacks.ebank.adapters.out.persistence.AccountJpaEntity airhacks.ebank.adapters.out.persistence.AccountMapper
- The boundary is already an adapter.
AccountsResource(HTTP) andAccountsTool(MCP) call the same control. Two delivery mechanisms share one control, without an interface between them. An inbound port would formalize a contract with one caller and one implementation. - The platform is not an external system. JPA, JAX-RS and CDI are the runtime. A port in front of
EntityManagerprotects against a database swap nobody plans. If Hibernate or PostgreSQL changed, the SQL, the annotations and the test setup would change anyway. - One entity instead of three.
Accountcarries JPA, JSON-B and OpenAPI annotations and has behavior likeisBalancePositive(). Hexagonal splits it into a domain object, a persistence model and a transport DTO, plus mappers in both directions. For five BCs that is roughly three times the classes. - Constraints by construction. The
reportingBC must not write. Hexagonal would express this as a query-only port. In eBankAccountQueryuses plain JDBC and never touches the persistence context. Apackage-info.javadocuments the reason. - Tests over the real protocol. The system test module calls the HTTP and MCP endpoints of the running application. These tests cover more than adapter tests against a mocked port. Business rules on the entity are unit tested without a container.
- Readability. Open
AccountsResource, follow one injection, reach the persistence call. With ports the path is interface, implementation, mapper.
The same properties matter for coding agents. An agent reads the package tree first. The BCE tree names the business (accounting, customers); the Hexagonal tree names the pattern (ports, adapters). Adding a field in eBank touches the entity and the boundary. In Hexagonal it touches the domain object, the JPA entity, the DTO and two mappers. Every additional file is a file the agent can forget, and every mapper is code the agent has to keep in sync. The requirements are placed next to the code: EARS statements (Easy Approach to Requirements Syntax) in package-info.java and a project annotation @Requirement on the boundary methods. The agent implements against the boundary contract and verifies with the system tests. The BCE rules are available as agent skills at airails.dev.
With CDI, a second implementation does not require Hexagonal either. Turning a control into an interface is an IDE refactoring: the injection points stay unchanged, and a qualifier, @Alternative, or a plain if statement selects the implementation. A second persistence technology affects only the controls that use it. Business rules on the entity are already tested without a database, and teams can own business components instead of layers.
See also: Alistair Cockburn's original description and the buckpal Hexagonal reference implementation. Both buckpal and eBank manage bank accounts, which makes the two package trees easy to compare.