Architecting Domain-Driven Applications with Next.js & Server Actions
How to separate business logic, validation schemas, and database repositories from React UI components for maintainable enterprise platforms—without turning your Next.js app into an unmaintainable blob.
Revilen Engineering
Systems Architecture · Revilen
Most teams do not set out to build a mess. They ship a feature under deadline, put a Prisma call inside a Server Action, validate with a few if-statements, and move on. Six months later the same file owns UI concerns, pricing rules, email side effects, and three half-finished migrations. At that point “just refactor later” is no longer a plan—it is a tax you pay on every release.
At Revilen we build operational platforms for logistics, manufacturing, advisory, and capital teams. Those systems change constantly: new approval paths, new integrations, new pricing exceptions. The only way to keep velocity is to make change local. Domain-driven structure is how we do that inside modern Next.js applications.
The real problem is not “clean code”—it is change cost
Clean-looking folders do not matter if a one-line business rule still forces you to touch five layers and re-test the entire checkout flow. Domain boundaries exist to answer a sharper question: when the business changes tomorrow, which files must move?
If the answer is “half the repo,” your architecture is already leaking. If the answer is “one domain module and its tests,” you can ship with confidence.
Technology should be built around the business—not the other way around. Architecture is how you keep that promise when requirements move.
— Revilen engineering principle
A practical layer map for Next.js apps
We do not cargo-cult hexagonal architecture diagrams into every repo. We use a thin, enforceable map that fits App Router reality:
- Presentation — routes, layouts, client components, forms. Knows how to render and collect input. Does not know SQL.
- Application — use-cases / Server Action orchestrators. Coordinates a single business operation end-to-end.
- Domain — entities, policies, invariants, pure calculations. No React. No Prisma. No fetch.
- Infrastructure — repositories, email, storage, third-party APIs, queue workers.
Server Actions sit at the edge of the application layer. They authenticate, parse input, call a use-case, and shape a result for the UI. They are not a dumping ground for business logic.
export async function createShipmentAction(input: unknown) {
const user = await requireSession();
const data = CreateShipmentSchema.parse(input);
return createShipmentUseCase({
actorId: user.id,
payload: data,
shipments: shipmentRepository,
events: eventBus,
});
}Validation belongs at the boundary—twice
Treat every inbound payload as hostile: form posts, webhooks, cron jobs, internal admin tools. Parse with a schema (Zod or equivalent) before domain code runs. That is your first boundary.
The second boundary is domain invariants. Schemas catch shape. Domains catch meaning. “Quantity must be a positive integer” is a schema rule. “You cannot ship more units than allocated inventory for this warehouse” is a domain rule. Mixing them creates brittle APIs and silent corruption.
- Schema validation: types, required fields, enums, string formats.
- Domain validation: business eligibility, state transitions, authorization relative to the entity.
- Infrastructure validation: foreign keys, unique constraints, transactional integrity as a last line of defense.
Repositories are adapters, not “the database folder”
A repository interface should speak the language of the domain: findOpenShipmentsForClient, reserveInventory, markInvoicePaid. The Prisma implementation translates that into tables and joins. When you need a read model for a dashboard, create a dedicated query—do not force every screen through the same write model.
This is also how you keep tests honest. Domain and use-case tests run against in-memory fakes. Infrastructure tests hit a real database in CI. You stop mocking half of Next.js just to assert that a discount rule works.
Vertical slices beat horizontal “utils” dumps
Organize by capability when the product grows: billing/, shipments/, workforce/, inbox/. Inside each slice, keep the same layering. Shared kernels stay tiny—identity, money, timestamps—not a 2,000-line helpers.ts that every feature imports.
A feature ships as: UI → action → use-case → repository → (optional) events. Code review becomes easier because reviewers can see the story of the change in one place.
What “done” looks like in production
- Business rules can be unit-tested without rendering React.
- Swapping Postgres providers or adding a queue does not rewrite domains.
- New engineers can locate where a rule lives in minutes.
- Incidents are diagnosable: logs point to a use-case name, not a 400-line action file.
Common failure modes we see in client codebases
- God Actions — one Server Action that does auth, validation, five queries, Stripe, and Slack.
- UI-owned rules — pricing or permissions computed only in components, reimplemented inconsistently on the server.
- Prisma as domain — table shapes leak into every feature, so renaming a column becomes a company-wide rewrite.
- Premature microservices — splitting deployables before splitting concepts. Distributed mess, same ball of mud.
How we apply this at Revilen
Whether we are building a workforce system, a unified inbox, or an operations platform that stitches Shopify, Amazon, and internal ERP data together, the same discipline holds. The UI can be beautiful. The agents can be clever. None of it matters if the core cannot absorb a new client workflow without a rewrite.
Domain-driven Next.js is not academic. It is how you keep a production company website and product stack honest as the business grows. Start with boundaries. Keep Server Actions thin. Put meaning in the domain. Let infrastructure be replaceable.
If you are staring at a Next.js app that already feels fragile, you do not need a rewrite from zero. Extract one painful use-case this week. Give it a schema, a domain function, and a repository. Then repeat. Architecture is a sequence of small, irreversible improvements—not a single heroic PR.