DRESS CODE had several products growing inside a monolith. We wanted APIs shaped for the UI, and we wanted business rules shaped around the domain. Splitting everything into microservices would have given us more deployment units, more operational work, and—if we were very lucky—the same problems over the network.
So the question became: can a monolith keep BFF-style concerns and domain concerns separate?

The two forces pulling an API apart #
A UI-driven endpoint gives a screen exactly what it needs:
GET /api/v1/dashboard
→ profile + recent order count + unread notificationsThat is efficient for the client. But if every endpoint grows directly from a screen, application code can collapse into CRUD and business rules spread across whichever handler needed them first. Eventually the database starts looking like a screenshot.

A domain-driven API goes the other way. It exposes stable resources such as /users/{id} and protects business rules, but a screen may need five calls and a small orchestration career to render itself. GraphQL or a Backend for Frontend can bridge the gap, though a separate BFF adds deployment and operations, and business logic has a habit of wandering into it after dark.


Separate the ideas, not the deployment #
The design keeps both modes inside one application:
- The presentation layer owns screen-shaped, read-only queries. A module may write the SQL needed for its view.
- The domain layer owns rules and state changes. Writes always travel through domain use cases.
- Shared domain query services cover reusable reads such as reference data or person lookup.

The hard line is side effects. Presentation code may assemble data freely, but it cannot perform writes. Any state change enters the domain path, where validation, invariants, and follow-up actions live.

A simplified package layout looks like this:
src/
└── api/
├── user-dashboard/
│ ├── controller/
│ ├── infrastructure/query-service/
│ └── usecase/
└── user-settings/
├── controller/
├── infrastructure/query-service/
└── usecase/

This resembles CQRS, with an asterisk #
The resemblance is real. Reads and writes follow different paths; UI queries are optimized as read models; domain logic controls the write model.
It is not strict CQRS. Both sides use the same database, and the domain still contains some query services. The goal was separation of BFF and domain thinking, not compliance with an architecture trading card. Names are useful until the team spends more time defending the name than the boundary.

The unresolved edges #
Three problems remain.
First, transactions currently begin in application use cases. Ideally aggregate boundaries would determine transaction scope, but that information is not always visible there. Domain operations currently return a Result and the use case owns the transaction.
Second, both paths share a schema. A domain-side database change can break a presentation query, so migrations must account for both consumers.
Third, returning domain entities directly from endpoints can leak new domain concepts into presentation code. Dedicated DTOs add mapping work but keep the boundary legible.
This is a staged migration, not a finished cathedral. The architecture lets the UI get efficient reads while the domain keeps authority over writes, and it does so without adding another production service. Event sourcing may eventually move it toward fuller CQRS. Until then, the monolith is doing useful work—which is usually a better success criterion than looking impressive in a diagram.