FRONTEND2026-04-05📖 7 min read

A Practical Guide to Applying Domain-Driven Design in Frontend Development

A Practical Guide to Applying Domain-Driven Design in Frontend Development

Learn how to apply Domain-Driven Design (DDD) to frontend development. From organizing business logic to component architecture, this guide presents practical approaches to improving system quality for small and medium-sized businesses.

髙木 晃宏

代表 / エンジニア

👨‍💼

"I only asked for a screen change—why is it taking so long?"—If you've ever wondered this about your software development team, you're not alone. The root cause in most cases is a misalignment between the business structure and the code structure. This article explains how to solve this problem by applying Domain-Driven Design (DDD) to frontend development, with concrete steps along the way.

For freelance engineers, DDD knowledge can significantly boost your proposal strength and design-phase presence. If you'd like to level up from "someone who can build screens" to "someone who can design business systems," read on.

What Is Domain-Driven Design (DDD)?

Domain-Driven Design (DDD) is a software design methodology proposed by Eric Evans in 2003. In a nutshell, it's the idea of "reflecting business language and structure directly in your code."

Traditional development typically starts from screen design or database design. However, this approach scatters business rules throughout the codebase, requiring widespread changes every time specifications change. In a past project of mine, the concept of "order" ended up with different definitions across the UI, API, and database, causing confusion with every modification.

In DDD, business experts and developers first define a shared vocabulary (ubiquitous language), then use those exact terms as class and function names in code. This way, when a business stakeholder says "I want to change this business rule," developers can immediately identify which part of the code needs modification.

ComparisonTraditional Screen-First DesignDomain-Driven Design
Design starting pointScreen layout / DB structureBusiness rules / business vocabulary
Impact scope of spec changesTends to ripple widelyLocalized to the relevant domain
Communication with business teamsRequires translation to technical termsDirect conversation using shared language
Initial design costLowSomewhat higher
Long-term maintenance costTends to increaseEasier to control

Key Components of DDD

DDD encompasses many concepts, but here are the ones most relevant to frontend application:

  • Entity: An object identified by a unique ID. For example, an "order" is distinguished by its order number and remains the same order even as its state changes.
  • Value Object: An object without an ID whose equality is determined by its values. "Address" and "monetary amount" fall into this category. Two separately created instances of "Tokyo, Minato-ku" are treated as the same value.
  • Aggregate: A unit grouping related entities and value objects. An "Order" aggregate manages the order entity, order line items, and shipping address as a cohesive whole.
  • Domain Service: Handles business logic that doesn't belong to any specific entity. For example, "merging two orders" is ambiguous about which order it belongs to, so it's defined as a domain service.
  • Repository: An abstraction layer for data persistence and retrieval. In frontend, extracting API interactions as repositories hides data access details from the domain layer.

You don't need to introduce all these concepts at once. Simply being mindful of the distinction between entities and value objects can dramatically improve code clarity.

Why Frontend Needs DDD

You might be thinking, "Isn't DDD a backend design methodology?" Indeed, DDD is most commonly discussed in server-side contexts, and I initially considered applying it to the frontend excessive. However, the landscape of frontend development has changed significantly in recent years.

Modern frontends are far more than display layers. With the proliferation of frameworks like React and Vue.js, frontends now handle complex state management and business logic. Consider a "cart" on an e-commerce site: adding items, removing items, changing quantities, calculating discounts, checking inventory—when all this logic is written directly in frontend components, modifications become error-prone.

In one project, discount logic was duplicated across three separate locations: the cart screen, the product listing screen, and the order confirmation screen. When the discount rate was changed, one location was missed, resulting in different prices showing on different screens. In hindsight, managing business logic by domain rather than by screen would have prevented this.

Challenges of Growing Frontend Complexity

In recent years, frontend responsibilities have accelerated dramatically. SPA (Single Page Application) adoption moved routing and session management to the frontend, and real-time communication and offline support are no longer unusual requirements.

Consider a concrete example. In one attendance management system project, the following logic all lived in the frontend:

  • Automatic work hour calculation: Break time deduction, night shift premiums, statutory holiday determination
  • Approval workflow: Approval route branching, state transitions on rejection
  • Real-time validation: Overtime limit checks based on labor regulations

All of this was written in a single components/AttendanceForm.tsx file spanning over 1,200 lines. A new team member who opened this file reportedly spent an entire day just figuring out "where does display logic end and business logic begin."

As a freelance engineer joining existing projects, encountering "business logic tightly coupled to UI" is not uncommon. With a DDD perspective, you can propose "let's start by organizing domain concepts" from day one, building trust with the team early on.

Practical Steps for Frontend DDD

So how do you concretely introduce DDD to the frontend? I recommend the following four-step approach.

Step 1: Define the Ubiquitous Language

Start by cataloging the vocabulary used in the business. Are "order," "purchase order," and "order" being used interchangeably? Do "customer" and "user" mean the same thing? This work must be done jointly by business stakeholders and developers.

// Example ubiquitous language definition (expressed as TypeScript types) type OrderStatus = 'draft' | 'confirmed' | 'shipped' | 'cancelled' interface Order { orderNumber: string customer: Customer lineItems: OrderLineItem[] status: OrderStatus totalAmount(): number }

While actual code follows English naming conventions, defining domain models in the business's native language during the design phase helps prevent misalignment with business stakeholders.

A tool we frequently use when defining the ubiquitous language is a glossary spreadsheet. We set up columns like the following and review them with all stakeholders:

Term (Business)Term (Code)DefinitionSynonyms / Confusing TermsBounded Context
OrderOrderThe act of a customer purchasing products, and its recordPurchase, POSales
CartCartProduct selection state before order confirmationShopping basketSales
CustomerCustomerA registered person with at least one purchaseUser, memberSales
Shipping AddressShippingAddressPhysical address where products are deliveredDelivery addressDelivery

Having this table alone naturally generates code review comments like "shouldn't this user be customer?" In one project, we embedded this glossary into the PR template and made domain terminology consistency a mandatory review checklist item.

Step 2: Restructure Your Directory Organization

Most frontend projects organize folders by technical role: components/, hooks/, utils/. When applying DDD thinking, organize folders by business domain instead.

src/ ├── domains/ │ ├── order/ # Order domain │ │ ├── models/ # Domain models (type definitions, validation) │ │ ├── services/ # Domain services (business logic) │ │ ├── components/ # Order-related UI components │ │ └── hooks/ # Order-related custom hooks │ ├── customer/ # Customer domain │ └── product/ # Product domain ├── shared/ # Cross-domain shared utilities └── pages/ # Pages (layer combining domains)

With this structure, "for order-related changes, look in order/" becomes the clear heuristic, making impact assessment dramatically easier.

Here's a more detailed directory structure for a real project. This example assumes the order domain of an e-commerce site:

src/domains/order/ ├── models/ │ ├── Order.ts # Order entity │ ├── OrderItem.ts # Order line item (value object) │ ├── OrderStatus.ts # Order status (value object) │ └── DiscountPolicy.ts # Discount policy (value object) ├── services/ │ ├── OrderCalculator.ts # Total and discount calculation │ ├── OrderValidator.ts # Business rule validation │ └── OrderFactory.ts # Order creation patterns ├── repositories/ │ └── OrderRepository.ts # API communication abstraction ├── components/ │ ├── OrderSummary.tsx # Order summary display │ ├── OrderItemList.tsx # Line item list │ └── OrderStatusBadge.tsx # Status badge └── hooks/ ├── useOrder.ts # Order retrieval and operations └── useOrderValidation.ts # Real-time validation

The key here is the repositories/ directory. By extracting API communication as a repository, you can adapt to backend API changes without affecting domain logic. When joining a project as a freelancer, the clarity of "look at the repository first to understand data flow" is tremendously helpful.

Step 3: Consolidate Business Logic in the Domain Layer

Separate business logic from UI components and consolidate it in the domain layer. Many developers share this pain point—components ballooning and becoming hard to navigate. This separation dramatically improves the situation.

Let's see the effect with a concrete example. Compare before and after extracting discount calculation logic from a component to the domain layer.

Before: Business logic mixed into the component

// components/CartSummary.tsx const CartSummary = ({ items, coupon }: Props) => { // Subtotal calculation const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0 ) // Discount calculation (business logic buried in UI) let discount = 0 if (coupon?.type === 'percentage') { discount = subtotal * (coupon.value / 100) } else if (coupon?.type === 'fixed') { discount = Math.min(coupon.value, subtotal) } // Member rank discount (more conditions pile up) if (memberRank === 'gold') { discount += subtotal * 0.05 } const total = subtotal - discount return ( <div> <p>Subtotal: {subtotal.toLocaleString()} JPY</p> <p>Discount: -{discount.toLocaleString()} JPY</p> <p>Total: {total.toLocaleString()} JPY</p> </div> ) }

After: Business logic consolidated in the domain layer

// domains/order/services/OrderCalculator.ts export class OrderCalculator { /** Calculates discount considering coupon and member rank */ static calculateDiscount( subtotal: number, coupon: Coupon | null, memberRank: MemberRank ): number { const couponDiscount = coupon ? CouponPolicy.apply(coupon, subtotal) : 0 const rankDiscount = MemberDiscountPolicy.apply(memberRank, subtotal) return couponDiscount + rankDiscount } static calculateTotal(items: OrderItem[]): OrderTotal { const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0 ) const discount = this.calculateDiscount(subtotal, /* ... */) return { subtotal, discount, total: subtotal - discount } } } // components/CartSummary.tsx (UI focuses solely on display) const CartSummary = ({ items, coupon }: Props) => { const { subtotal, discount, total } = OrderCalculator.calculateTotal(items) return ( <div> <p>Subtotal: {subtotal.toLocaleString()} JPY</p> <p>Discount: -{discount.toLocaleString()} JPY</p> <p>Total: {total.toLocaleString()} JPY</p> </div> ) }

In the "After" code, when discount rules change, only OrderCalculator needs modification—the display component remains untouched. This separation also makes unit testing dramatically easier, since you can test business logic as pure functions without worrying about UI rendering.

Step 4: Establish Bounded Contexts

The word "customer" might mean "including prospects" in the sales department while meaning "contracted customers only" in the support department. Clearly separating domains where the same term carries different meanings as "bounded contexts" prevents confusion.

Let me illustrate more concretely how this applies to the frontend. In one recruiting service project, the word "user" was used with three different meanings:

  • Job seeker context: A person applying for jobs. Has a skills profile and job preferences.
  • Employer context: A company representative posting jobs. Has permission levels and department affiliation.
  • Admin context: A system administrator. Has activity logs and permission settings.

When these are all treated as the same User type in code, an implicit assumption emerges: "whether this User has a skills profile property depends on the calling context." This is a breeding ground for bugs.

// Separate types by context // domains/applicant/models/Applicant.ts interface Applicant { id: string name: string skillSheet: SkillSheet preferences: JobPreferences } // domains/recruiter/models/Recruiter.ts interface Recruiter { id: string name: string company: Company department: string permissionLevel: PermissionLevel }

By explicitly separating types this way, mistakes like "I thought I was handling job seeker data, but an employer type was passed in" can be caught at TypeScript compile time.

Common Pitfalls and Solutions When Adopting DDD

Based on our experience across projects, here are common issues that arise during DDD adoption. I'll share the scenarios where things didn't go well from the start.

The problem of excessive abstraction. When you try to faithfully implement every DDD concept, code volume balloons and development speed actually drops. Starting with just "ubiquitous language unification" and "domain-based directory structure" is the most realistic approach. There are many situations where choosing between approaches requires judgment based on team size and project complexity.

Here's a specific failure we experienced. In one SaaS project, we tried implementing all data updates as "domain events." A single button click would flow through event publishing, subscribing, and handling across three files, causing development velocity to plummet. We ultimately limited event-driven patterns to where they were truly needed (cross-domain operations) and reverted to simple function calls, which restored team productivity.

The challenge of permeating the whole team is also significant. To maximize DDD's benefits, not just developers but business team members need to use the ubiquitous language. Establishing regular terminology review sessions enables early detection of vocabulary drift.

Here are some tactics that proved effective for permeation:

  • Terminology checks in PR reviews: Including "does this variable name match the ubiquitous language?" as a review checklist item
  • Monthly modeling sessions: Business stakeholders and engineers gather to update domain model diagrams together. Using sticky notes on a whiteboard often reveals surprising misalignments
  • Glossary CI integration: Introducing custom ESLint rules that detect prohibited terms (e.g., using user when the ubiquitous language specifies "customer")

Gradual adoption into existing codebases is another common concern. Completely restructuring an already-running project into DDD architecture at once isn't realistic. The recommended approach is "apply DDD structure to new features, and migrate existing code gradually during maintenance." In one project, we planned to focus on one domain per quarter for intensive refactoring, completing the migration of four major domains over a year.

Here's a checklist to help determine whether adoption makes sense:

  • Do business rule changes occur frequently?
  • Is the application larger than 10 screens?
  • Are multiple business departments involved?
  • Is 1+ year of ongoing operation and maintenance expected?
  • Do spec changes regularly cause unexpected bugs?

If three or more apply, DDD adoption is likely to yield significant benefits. Conversely, for small, short-term projects, the design overhead of DDD may not be justified.

Frontend DDD and Testing Strategy

One of DDD's major benefits is improved testability. When business logic is consolidated in the domain layer, you can test logic independently from UI rendering.

Domain Layer Unit Tests

Using the OrderCalculator from earlier, tests look like this:

describe('OrderCalculator', () => { describe('discount calculation', () => { it('applies 10% discount with a percentage coupon', () => { const coupon = { type: 'percentage', value: 10 } as Coupon const discount = OrderCalculator.calculateDiscount(10000, coupon, 'regular') expect(discount).toBe(1000) }) it('caps fixed-amount coupon at the subtotal when it exceeds it', () => { const coupon = { type: 'fixed', value: 5000 } as Coupon const discount = OrderCalculator.calculateDiscount(3000, coupon, 'regular') expect(discount).toBe(3000) }) it('applies an additional 5% discount for gold members', () => { const discount = OrderCalculator.calculateDiscount(10000, null, 'gold') expect(discount).toBe(500) }) }) })

These tests require no React rendering, no mock servers. Business rule correctness is verified through pure input-output testing alone. Test execution is fast, so running them frequently during development isn't burdensome.

Keep UI Component Tests Simple

After moving business logic to the domain layer, UI component tests can be narrowed down to "is the correct data being displayed?" Complex conditional logic is already tested in the domain layer, so verifying display alone is sufficient at the component level.

This separation is also helpful when joining a project as a freelancer. When domain tests and UI tests are clearly separated, you can confidently verify the impact of your changes within your area of responsibility.

How Freelance Engineers Can Leverage DDD Skills

While this article has focused primarily on technical content, for freelance engineers, DDD knowledge is a powerful career asset.

Differentiation During Proposal Phase

Being able to propose at an initial client meeting, "We'll organize your business processes and reflect that structure directly in the code using a proven design methodology"—engineers who can do this are still rare. Beyond looking at design mockups and saying "I can build that," being able to ask "how should we divide this business into domains to make future spec changes easier?" becomes leverage for rate negotiation as well.

In our own hiring interviews, what we value most is the ability to discuss "how to structure this business" rather than "how to lay this out on screen."

Improvement Proposals for Existing Projects

When you discover "components are bloated and modifications are painful" in a project you've joined, a DDD perspective lets you present concrete improvement plans.

For example, proposing "let's first extract business logic into the domain layer, then refactor one domain per quarter" makes the investment-return ratio visible to the client. "I want to refactor things" is hard to get budget for, but "improving the order domain can reduce discount-related bug resolution by an estimated 10 hours per month" makes approval much more likely.

Code as Design Documentation

A codebase designed with DDD principles functions as documentation in itself. The directory structure reveals the domain landscape, and type definitions convey business rules. During handoffs, you can explain succinctly, "this folder corresponds to this business area," enabling smooth transitions when leaving a project as a freelancer.

DDD Small Start: Three Things You Can Do Tomorrow

For those interested in DDD, here are three actions you can take tomorrow without major design changes.

1. Align variable and function names with business terminology

Simply replacing generic names like data, info, and item with business terms like order, customer, and shippingAddress makes a difference. Anyone reading the code can immediately understand "what data is this."

2. Extract business logic from one component

Choose the most bloated component in your project and extract its business logic into a separate file. You don't need to do everything at once. If you can demonstrate the effect with a single component, it creates momentum for adoption within the team.

3. Create a single-page glossary

Compile even just 10 business terms used in your project into a spreadsheet and share it with the team. The discoveries of "we actually had different understandings of this term within the team" will be surprisingly numerous.

Conclusion: Design That Connects Business and Code Becomes a Long-Term Asset

By incorporating Domain-Driven Design into frontend development, business vocabulary is directly reflected in code structure, dramatically improving adaptability to specification changes. While the initial design does require time investment, the long-term return through reduced maintenance costs and stabilized quality makes it worthwhile.

What matters is not aiming for perfect DDD, but gradually incorporating business-aware design. I recommend starting with ubiquitous language organization.

For freelance engineers, DDD knowledge can be a turning point from "someone who builds" to "someone who designs." The ability to deeply understand a client's business and translate it into maintainable code is a major advantage in both winning projects and securing ongoing engagements.

At aduce, we offer consulting on business-aware frontend design and DX initiatives. If you're facing challenges with existing system design reviews or new development design strategies, feel free to contact us.