はじめに
Desksetappを用いて、一部の処理をDDDで処理を実行することにしました。
そもそも、DDDとは?
DDDとは、Domain Driven Designといい、日本語で「ドメイン駆動設計」と言います。
ソフトウェアの設計・開発において、業務領域(ドメイン)の知識を中心に考える方法です。
ざっくり下のような図でよく表されています。
なぜDDDを用いて開発するのか
アクティブユーザーを見定めることでお客の属性を知ることができるうえに、今後の開発でお客様に新商品の提案をスムーズにすることが可能なためです。
設計
設計するのは、以下になります。
- Domain
- UseCase
- Port
- Gateway
- Driver
Domain
Domainが業務ルールなるコアの部分となります。要は肝になるところです。
今回は、ユーザーのなかでも購入回数が1回以上の人をアクティブユーザーとすることにしました。(購入したことがあればアクティブユーザーとしています。)
UseCase
アプリケーションの処理の流れを組み立てる部分です。Gatewayのデータを用いて、アクティブユーザーを抽出します。
Port
UseCaseが必要とするデータ取得のインターフェースを定義します。
Driver
外部DBやAPIからデータを取得します。今回はSupabaseから取得しています。
仮にAPIから取得する場合はfetchなどを使用し取得します。
Gateway
Driverから取得したデータをUseCaseやDomainが使いやすいように加工した状態にするのがGatewayになります。
必要なデータだけを選別するなどもしています。
Domain
システムの肝になる部分です。購入したことあるユーザーをアクティブユーザーと設定しているため以下と記載しました。
export type User = {
id: number;
name: string;
}
export function isActiveUser(purchases: number): boolean {
return purchases >= 1;
}
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
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;
}
}
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,
}));
}
}
import type { User } from "../../domain/User";
export interface UserPort {
getUser(): Promise<User[]>;
}
UseCase
Port部分のデータを使って、Domain部分にデータを渡す役割をしています。
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);
});
}
}
また、テストケースも実行し動作することを確認しています。
テストコード
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);
});
コード
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で実装するさいの肝や考慮もありますが、さらに追及していきます。
参考文献



