3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

DDDを用いた開発と難しさ

3
Last updated at Posted at 2026-09-12

はじめに

Desksetappを用いて、一部の処理をDDDで処理を実行することにしました。

そもそも、DDDとは?

DDDとは、Domain Driven Designといい、日本語で「ドメイン駆動設計」と言います。

ソフトウェアの設計・開発において、業務領域(ドメイン)の知識を中心に考える方法です。
ざっくり下のような図でよく表されています。

https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_670219_3e2eaa70-ba18-73df-2bf3-ef86212120f9.avif

なぜDDDを用いて開発するのか

アクティブユーザーを見定めることでお客の属性を知ることができるうえに、今後の開発でお客様に新商品の提案をスムーズにすることが可能なためです。

設計

設計するのは、以下になります。

  • Domain
  • UseCase
  • Port
  • Gateway
  • Driver

Domain

Domainが業務ルールなるコアの部分となります。要は肝になるところです。
今回は、ユーザーのなかでも購入回数が1回以上の人をアクティブユーザーとすることにしました。(購入したことがあればアクティブユーザーとしています。)

UseCase

アプリケーションの処理の流れを組み立てる部分です。Gatewayのデータを用いて、アクティブユーザーを抽出します。

Port

UseCaseが必要とするデータ取得のインターフェースを定義します。

Driver

外部DBやAPIからデータを取得します。今回はSupabaseから取得しています。
仮にAPIから取得する場合はfetchなどを使用し取得します。

Gateway

Driverから取得したデータをUseCaseやDomainが使いやすいように加工した状態にするのがGatewayになります。
必要なデータだけを選別するなどもしています。

相関図で示すと以下になります。
mermaid-diagram.png

Domain

システムの肝になる部分です。購入したことあるユーザーをアクティブユーザーと設定しているため以下と記載しました。

User
export type User = {
    id: number;
    name: string;
}

export function isActiveUser(purchases: number): boolean {
    return purchases >= 1;
}

Purchase
export type Purchase = {
    id: number;
    user_id: number;
    purchased_at: Date;
};

Port、Gateway、Driver

外部DBやAPIから情報を取得し(Driver部分)、加工する部分(Gateway部分)にあたります。
今回は開発用にユーザーTBL(user_infor_dev)と購入履歴TBL(purchases_dev)を設定しました。

user_infor_dev

Name Type option
id int8
name varchar
address varchar
tel varchar
age int8

purchases_dev

Name Type option
id int8
user_infor_dev_id int8
purchased_at timestamp

2つのTBL関係は以下の通りです。
user_infor_dev.id === purchases_dev.user_infor_dev_id

desksetappDBInfor
import { supabase } from "../../utils/supabase";

export type UserInfor = {
    id: number;
    name: string;
    address: string | null;
    tel: string | null;
    age: number | null;
    created_at: Date;
}

export type PurchaseInfor = {
    id: number;
    user_infor_dev_id: number;
    purchased_at: Date | null;
    created_at: Date;
}

export class DesksetappDBInfor {
    async UserPort(): Promise<UserInfor[]> {
        const useInforResponse = await supabase.from("user_infor_dev").select("*");
        console.log(useInforResponse);
        return useInforResponse.data;
    }
    async PurchasePort(): Promise<PurchaseInfor[]> {
        const purchaseResponse = await supabase.from("purchases_dev").select("*");
        console.log(purchaseResponse);
        return purchaseResponse.data;
    }
}
userGateway
import { UserPort } from "../usecase/port/UserPort";
import { User } from "../domain/User";
import { DesksetappDBInfor } from "../driver/desksetappDBInfor";


export class UserGateway implements UserPort {
    constructor(private readonly desksetappDBInfor: DesksetappDBInfor) {}
    async getUser(): Promise<User[]> {
        const userInfor = await this.desksetappDBInfor.UserPort();
        return userInfor.map((user) => ({
            id: user.id,
            name: user.name,
        }));
    }
}

UserPort
import type { User } from "../../domain/User";

export interface UserPort {
    getUser(): Promise<User[]>;
}

UseCase

Port部分のデータを使って、Domain部分にデータを渡す役割をしています。

ActiveUserUsecase
import { isActiveUser, type User } from "../domain/User";
import type { UserPort } from "./port/UserPort";
import type { PurchasePort } from "./port/PurchasePort";

export class ActiveUserUsecase {
    constructor(
        private readonly userPort: UserPort,
        private readonly purchasePort: PurchasePort
    ){};

    async execute(): Promise<User[]> {
        const user = await this.userPort.getUser();
        const purchases = await this.purchasePort.getPurchase();

        return user.filter((user) => {
            const userPurchases = purchases.filter((purchase) => purchase.user_id === user.id);
            return isActiveUser(userPurchases.length);
        });
    }
}

また、テストケースも実行し動作することを確認しています。

テストコード
ActiveUserUsecase.test
import {test, expect, describe, beforeEach, vi} from "vitest";
import { ActiveUserUsecase } from "../usecase/ActiveUserUsecase";
import type { UserPort } from "../usecase/port/UserPort";
import { PurchasePort } from "../usecase/port/PurchasePort";

test("購入履歴が1件以上あるユーザーをアクティブユーザーとして返す", async () => {
    const mockUserPort: UserPort = {
        getUser: async () => [
            { id: 1, name: "John Doe" },
        ]
    };
    const mockPurchasePort: PurchasePort = {
        getPurchase: async () => [
            { id: 1, user_id: 1, purchased_at: new Date("2026-09-10T09:20:53+00:00") },
        ]
    };

    const useCase = new ActiveUserUsecase(mockUserPort, mockPurchasePort);
    const expected = [{ id: 1, name: "John Doe" }];
    const actual = await useCase.execute();
    expect(actual).toEqual(expected);
});

画面から今回のデータ取得ができていることも確認しています。
image.png

コード
page
import { headers } from 'next/headers';
import { UserGateway } from "../../../src/gateway/userGateway";
import { PurchaseGateway } from "../../../src/gateway/purchaseGateway";
import { ActiveUserUsecase } from "../../../src/usecase/ActiveUserUsecase";
import { DesksetappDBInfor } from "../../../src/driver/desksetappDBInfor";


export default async function DDDTestPage() {
    const desksetappDBInfor = new DesksetappDBInfor();
    const userGetway = new UserGateway(desksetappDBInfor);
    const purchaseGetway = new PurchaseGateway(desksetappDBInfor);
    const usecase = new ActiveUserUsecase(userGetway, purchaseGetway);
    const data = await usecase.execute();
    return (
        <>
            <div>DDD test completed</div>
            <p>アクティブユーザ: {JSON.stringify(data)}</p>
        </>
    );
    

}

DDD開発の難しさ

DDDのメリットとして、言語やDBに左右されずに業務ルールを設定できるという点があります。
開発を通じ、以下のことが困難だと感じました。

  • 業務ルールの設定方法
  • どこまでをDDDに適用するか
  • DB上の問題

開発ではアクティブユーザーを1回以上としましたが、のちに、キャンセル・返品・無料注文などを購入回数に含めるかどうかを確認する必要があります。
また、ユーザーからの変更により直近1か月で2回以上の購入となると、Domainを修正することになります。
さらに、調整も難しく、設計前の段階で苦労します。
DBの取得では、ユーザー数が多い場合、N+1問題、UpdateやInsertのとき、冪等性担保やSagaパターンを確保する必要があります。その点も調整が困難だと感じました。

おわりに

DDDで実装するさいの肝や考慮もありますが、さらに追及していきます。

参考文献

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?