이코에코(Eco²)/Clean Architecture Migration
이코에코(Eco²) Clean Architecture #6: My->Users 리팩토링.MD
mango_fr
2026. 1. 1. 01:38

작성일: 2025-12-31
Opus 4.5와의 문답을 거치며 작성한 문서이며 My->Users 리팩토링 과정의 초안이 됩니다.
핵심 변경 사항
| 변경 | AS-IS | TO-BE |
|---|---|---|
| 도메인명 | my |
users |
| 라우팅 | /api/v1/user/me |
/api/v1/users/me |
| User 소유권 | auth + my (분산) | users (단일) |
| auth -> 회원가입 | 직접 DB 접근, Auth에서 수행 | gRPC, Users에서 수행 |
마이그레이션 목표
| 목표 | 설명 |
|---|---|
| 계층 분리 | Presentation, Application, Domain, Infrastructure 분리 |
| 의존성 역전 | Port/Adapter 패턴으로 외부 의존성 추상화 |
| gRPC 통합 | auth ↔ users 도메인 간 gRPC 통신, 도메인 명확히 분리 |
| CQRS 적용 | 읽기(Query)와 쓰기(Command) 분리 |
| 데이터 소유권 명확화 | 중복 테이블 통합, 단일 소유 |
현재 구조 분석
데이터 흐름

데이터 흐름 설명:
- HTTP API: 클라이언트에서 프로필 조회/수정/삭제, 캐릭터 목록 조회
- gRPC Server:
character도메인에서 캐릭터 지급 시GrantCharacterRPC 호출 (sync scan, deprecated) - Celery Worker:
scan도메인에서 보상 처리 시my.save_character태스크 발행 → 배치로 DB 저장 (async scan) - Auth 참조:
auth도메인의users,user_social_accounts테이블을 읽기 전용으로 참조
핵심 구성요소
| 구성요소 | 역할 | 프로토콜 |
|---|---|---|
| my-api | 프로필 CRUD + 캐릭터 조회 | HTTP |
| my-grpc | 캐릭터 지급 | gRPC |
| my-worker | 캐릭터 배치 저장 | Celery |
현재 디렉토리 구조
domains/my/
├── main.py # FastAPI 앱
├── api/v1/endpoints/ # HTTP 엔드포인트
│ ├── profile.py
│ └── characters.py
├── services/ # 비즈니스 로직
│ ├── my.py # 프로필 서비스
│ └── characters.py # 캐릭터 서비스
├── repositories/ # 데이터 액세스
│ ├── user_repository.py
│ ├── user_character_repository.py
│ └── user_social_account_repository.py
├── models/ # SQLAlchemy ORM
│ ├── user.py
│ ├── user_character.py
│ ├── auth_user.py # auth 참조
│ └── auth_user_social_account.py
├── rpc/ # gRPC
│ ├── server.py # gRPC 서버
│ ├── character_client.py # character gRPC 클라이언트
│ └── v1/
│ └── user_character_servicer.py
├── tasks/ # Celery 태스크
│ └── sync_character.py
├── schemas/ # Pydantic 스키마
└── proto/ # Protobuf 정의
스키마 분리의 배경과 문제점
원래 설계 의도
초기 설계에서는 마이크로서비스 지향 아키텍처를 염두에 두고 스키마를 분리했다.
| 원칙 | 의도 |
|---|---|
| Bounded Context | 각 도메인이 자신의 데이터를 완전히 소유 |
| 스키마 분리 | 나중에 DB 분리를 쉽게 하기 위해 |
| 느슨한 결합 | user_profile.users.auth_user_id로 논리적 참조만 |
원래 의도한 데이터 분리:
auth.users: 최소한의 인증 정보 (id, 소셜 계정 연결용)user_profile.users: 확장된 프로필 정보 (닉네임, 전화번호, 프로필 이미지)
실제로 발생한 문제
문제 1: 데이터 중복

중복 발생 원인
| 시점 | 상황 | 결과 |
|---|---|---|
| OAuth 구현 | OAuth에서 닉네임/이미지 제공 → auth.users에 저장 |
auth에 프로필 정보 추가 |
| 프로필 기능 | my 도메인도 프로필 필요 → user_profile.users 생성 |
같은 정보 중복 저장 |
문제 2: 애플리케이션 레벨 동기화 필요
# my/services/my.py — 프로필 수정 시 양쪽 자동 업데이트
async def _apply_update(self, user: User, payload: UserUpdate) -> User:
# 1. user_profile.users 업데이트
updated = await self.repo.update_user(user, update_data)
# 2. auth.users도 자동 업데이트 (애플리케이션에서 처리)
if "phone_number" in update_data:
await self.repo.update_auth_user_phone(
user.auth_user_id, update_data.get("phone_number")
)
await self.session.commit()
문제
- 사용자 입장에선 자동이지만, 명시적으로 구현해 유지보수 부담 상승
- 새 필드 추가 시 동기화 로직도 함께 추가해야 함
- 다른 도메인(예: admin)에서 수정 시 동기화 누락 위험
- 트랜잭션 범위가 두 스키마에 걸쳐있음
문제 3: 불분명한 데이터 소유권
| 질문 | 답변 불가 |
|---|---|
| 닉네임은 누구 책임? | auth.users.nickname vs user_profile.users.nickname |
| 프로필 수정 시 어디를 업데이트? | 둘 다? |
| 두 값이 다르면 어느 게 진짜? | 명확하지 않음 |
적용 방향
- users 도메인이 User 전체 소유
- auth는 인증/토큰만, auth.user 스키마 삭제
- gRPC로 통신, DB 직접 참조 X
현재 스키마 구조 (ER Diagram)

ER 다이어그램 설명:
- auth.users: 인증 도메인의 핵심 사용자 테이블 — OAuth 로그인 시 생성
- auth.user_social_accounts: 소셜 로그인 계정 — 1:N 관계로 다중 소셜 계정 지원
- user_profile.users: 프로필 확장 테이블 —
auth_user_id로 auth.users 참조 - user_profile.user_characters: 캐릭터 인벤토리 — FK 없이
user_id로 논리적 참조
중복 필드 테이블
| 필드 | auth.users | user_profile.users | 문제 |
|---|---|---|---|
username |
✅ | ✅ | 중복 |
nickname |
✅ | ✅ | 중복 |
phone_number |
✅ | ✅ | 중복 |
profile_image_url |
✅ | ✅ | 중복 |
name |
❌ | ✅ | 확장 필드 |
email |
❌ (소셜 계정에 있음) | ✅vice | 확장 필드 |
현재 동기화 로직 (MyService._apply_update):
# phone_number 수정 시 양쪽 테이블 자동 동기화 (애플리케이션 레벨)
if "phone_number" in update_data:
await self.repo.update_auth_user_phone(
user.auth_user_id, update_data.get("phone_number")
)
await self.session.commit()
→ 애플리케이션 레벨 동기화로 인한 유지보수 부담 및 불일치 위험
gRPC 기반 도메인 통합 설계
목표 아키텍처

아키텍처 설명
- auth 도메인: 인증/토큰만 담당, User 데이터 직접 접근 ❌
- users 도메인: User 전체 소유, gRPC 서버 제공
- 통신 방식: auth → users는 gRPC 동기 호출 (OAuth 콜백은 이벤트/비동기 처리 불가, 즉시 응답이 가능한 강결합)
OAuth 콜백 플로우 (gRPC)

| 동기 호출 근거 | 설명 |
|---|---|
| 토큰 발급 필요 | JWT에 user_id 클레임 포함 필수 |
| 즉시 응답 필요 | 사용자에게 토큰과 함께 리다이렉트 |
| 롤백 필요 | 사용자 생성 실패 시 로그인 실패 |
최종 Users 스키마

ERD 설명
| 관계 | 유형 | 설명 |
|---|---|---|
users ↔ user_social_accounts |
has (1:N, FK) |
실제 FK, CASCADE 삭제 |
users ↔ user_characters |
owns (1:N, FK) |
실제 FK, CASCADE 삭제 |
user_characters ↔ characters |
논리 FK | FK 없음, 도메인 독립성 |
도메인별 책임 정리
| 도메인 | 역할 | 테이블 접근 |
|---|---|---|
| users | User 전체 소유, CRUD, 프로필, 캐릭터 | Read/Write |
| auth | 인증, 토큰 발급 | gRPC 통해서만 |
| character | 캐릭터 지급 요청 | gRPC 통해서만 |
| scan | 보상 처리 후 캐릭터 지급 | Celery/gRPC |
제약조건 요약
| 테이블 | 제약조건 | 컬럼 | 목적 |
|---|---|---|---|
users |
uq_users_phone |
phone_number |
전화번호 중복 방지 |
user_social_accounts |
uq_social_identity |
(provider, provider_user_id) |
소셜 계정 중복 방지 |
user_social_accounts |
fk_social_user |
user_id |
CASCADE 삭제 |
user_characters |
uq_user_character |
(user_id, character_code) |
캐릭터 중복 소유 방지 |
user_characters |
fk_character_user |
user_id |
CASCADE 삭제 |
user_characters |
chk_status |
status |
값 검증 (owned/burned) |
스키마 통합 마이그레이션 계획
AS-IS vs TO-BE

마이그레이션 단계
| Phase | 작업 | 테이블 변경 | 다운타임 |
|---|---|---|---|
| 1 | 코드 이동 (auth → users) + gRPC 추가 | ❌ | ❌ |
| 2 | 스키마 생성 및 테이블 이동 | ✅ | 짧음 |
| 3 | 중복 데이터 병합 | ✅ | 짧음 |
| 4 | 이전 테이블/스키마 삭제 | ✅ | ❌ |
Phase 2: 스키마 마이그레이션 SQL
-- Step 1: 새 스키마 생성
CREATE SCHEMA IF NOT EXISTS users;
-- Step 2: auth 테이블 이동
ALTER TABLE auth.users SET SCHEMA users;
ALTER TABLE auth.user_social_accounts SET SCHEMA users;
-- Step 3: user_profile.users 데이터 병합
UPDATE users.users u
SET
name = up.name,
email = COALESCE(u.email, up.email)
FROM user_profile.users up
WHERE u.id = up.auth_user_id;
-- Step 4: user_characters 이동
ALTER TABLE user_profile.user_characters SET SCHEMA users;
-- Step 5: 중복 테이블 삭제
DROP TABLE user_profile.users;
DROP SCHEMA user_profile;
-- Step 6 (선택): auth 스키마 삭제
DROP SCHEMA auth;
기능 분석
API 엔드포인트
| 엔드포인트 | 메서드 | 설명 | 서비스 |
|---|---|---|---|
/user/me |
GET | 현재 사용자 프로필 조회 | UserService |
/user/me |
PATCH | 닉네임, 전화번호 수정 | UserService |
/user/me |
DELETE | 계정 삭제 | UserService |
/user/me/characters |
GET | 소유 캐릭터 목록 | UserCharacterService |
/user/me/characters/{name}/ownership |
GET | 특정 캐릭터 소유 여부 | UserCharacterService |
GrantCharacter (gRPC) |
- | 캐릭터 지급 (character 도메인에서 호출) | UserCharacterServicer |
캐릭터 저장 흐름 (Celery Batches)

Celery Batches 흐름 설명:
- 메시지 발행:
scan도메인에서 보상 처리 시my.save_character태스크 발행 - 배치 축적:
flush_every=50(50개 모이면) 또는flush_interval=5s(5초 경과) 시 배치 처리 - BULK UPSERT:
ON CONFLICT (user_id, character_code) DO UPDATE로 멱등성 보장 - Self-Healing:
character_id캐시 불일치 시에도character_code기준으로 정확히 저장
gRPC 서버 (GrantCharacter, deprecated)

character도메인에서 동기 스캔 처리 시GrantCharacterRPC 호출- Optimistic Locking으로 동시 요청 시에도 중복 지급 방지
IntegrityError발생 시already_owned=true반환
외부 도메인 참조

외부 참조 설명:
| 참조 대상 | 용도 | 방식 |
|---|---|---|
auth.users |
사용자 기본 정보 (닉네임, 이메일) | PostgreSQL 직접 조회 (읽기 전용) |
auth.user_social_accounts |
소셜 계정 정보 (provider, last_login) | PostgreSQL 직접 조회 (읽기 전용) |
character 도메인 |
기본 캐릭터 정보 조회 | gRPC 클라이언트 (Circuit Breaker 적용) |
Clean Architecture 마이그레이션
목표 아키텍처

아키텍처 설명:
- Presentation: HTTP Controller와 gRPC Servicer가 공존 — 프로토콜 추상화
- Application: CQRS 패턴 적용 — Commands(쓰기), Queries(읽기) 분리
- Domain: 순수 비즈니스 로직 — 외부 의존성 없음
- Infrastructure: Port 구현체 — SQLAlchemy, gRPC 클라이언트
의존성 방향

| 규칙 | 설명 |
|---|---|
| 안쪽으로만 | 모든 의존성은 Domain을 향함 |
| Port/Adapter | Application이 Port 정의, Infrastructure가 구현 |
| DIP 적용 | 상위 모듈이 하위 모듈에 의존하지 않음 |
계층별 상세 설계
Presentation Layer
HTTP Controllers
| Legacy | Clean Architecture | 역할 |
|---|---|---|
api/v1/endpoints/profile.py |
presentation/http/controllers/profile.py |
프로필 CRUD |
api/v1/endpoints/characters.py |
presentation/http/controllers/characters.py |
캐릭터 조회 |
# presentation/http/controllers/profile.py
@router.get("/me", response_model=ProfileResponse)
async def get_profile(
user: UserInfo = Depends(get_current_user),
query: GetProfileQuery = Depends(),
) -> ProfileResponse:
dto = await query.execute(
GetProfileRequest(user_id=user.user_id, provider=user.provider)
)
return ProfileResponse.from_dto(dto)
gRPC Servicer
| Legacy | Clean Architecture | 역할 |
|---|---|---|
rpc/v1/user_character_servicer.py |
presentation/grpc/user_character_servicer.py |
캐릭터 지급 RPC |
# presentation/grpc/user_character_servicer.py
class UserCharacterServicer(user_character_pb2_grpc.UserCharacterServiceServicer):
def __init__(self, grant_command: GrantCharacterCommand):
self._command = grant_command
async def GrantCharacter(self, request, context) -> GrantCharacterResponse:
dto = GrantCharacterRequest.from_proto(request)
result = await self._command.execute(dto)
return result.to_proto()
Application Layer
Commands (쓰기)
| Use Case | 설명 | 의존 Port |
|---|---|---|
UpdateProfileCommand |
프로필 수정 | UserGateway |
DeleteUserCommand |
계정 삭제 | UserGateway |
GrantCharacterCommand |
캐릭터 지급 (gRPC) | UserCharacterGateway |
# application/commands/grant_character.py
@dataclass
class GrantCharacterRequest:
user_id: UUID
character_id: UUID
character_code: str
character_name: str
character_type: str | None
character_dialog: str | None
source: str | None
class GrantCharacterCommand:
def __init__(self, gateway: UserCharacterGateway):
self._gateway = gateway
async def execute(self, request: GrantCharacterRequest) -> GrantCharacterResult:
result = await self._gateway.upsert(
user_id=request.user_id,
character_id=request.character_id,
character_code=request.character_code,
# ...
)
return GrantCharacterResult(
success=True,
already_owned=result.was_existing,
)
Queries (읽기)
| Use Case | 설명 | 의존 Port |
|---|---|---|
GetProfileQuery |
프로필 조회 | UserGateway, AuthUserReader |
ListCharactersQuery |
캐릭터 목록 | UserCharacterGateway |
CheckOwnershipQuery |
소유 확인 | UserCharacterGateway |
Ports (인터페이스)
# application/common/ports/user_character_gateway.py
class UserCharacterGateway(Protocol):
async def upsert(
self,
*,
user_id: UUID,
character_id: UUID,
character_code: str,
character_name: str,
character_type: str | None,
character_dialog: str | None,
source: str | None,
) -> UpsertResult:
"""멱등 UPSERT (character_code 기준)"""
...
async def list_by_user(self, user_id: UUID) -> list[UserCharacterDTO]:
...
async def check_ownership_by_name(self, user_id: UUID, name: str) -> bool:
...
# application/common/ports/character_client.py
class CharacterClient(Protocol):
async def get_default_character(self) -> DefaultCharacterDTO | None:
"""기본 캐릭터 조회 (Circuit Breaker 적용)"""
...
Domain Layer
Entity
# domain/entities/user.py
@dataclass
class User:
id: int | None
auth_user_id: UUID
username: str | None
nickname: str | None
phone_number: PhoneNumber | None # Value Object
profile_image_url: str | None
created_at: datetime | None
def update_profile(
self,
nickname: str | None = None,
phone_number: PhoneNumber | None = None,
) -> None:
if nickname is not None:
self.nickname = nickname
if phone_number is not None:
self.phone_number = phone_number
Value Object
# domain/value_objects/phone_number.py
@dataclass(frozen=True, slots=True)
class PhoneNumber:
value: str
def __post_init__(self):
normalized = self._normalize(self.value)
if not self._is_valid(normalized):
raise InvalidPhoneNumberError(self.value)
object.__setattr__(self, 'value', normalized)
@staticmethod
def _normalize(raw: str) -> str:
digits = re.sub(r"\D+", "", raw)
if digits.startswith("82") and len(digits) >= 11:
digits = "0" + digits[2:]
return digits
def formatted(self) -> str:
"""010-1234-5678 형식"""
return f"{self.value[:3]}-{self.value[3:7]}-{self.value[7:]}"
Value Object 설명:
- 불변(Immutable):
frozen=True로 생성 후 변경 불가 - 자가 검증: 생성 시점에 유효성 검사
- 정규화: 국제 형식(+82) → 국내 형식(010) 자동 변환
Domain Service
# domain/services/profile_service.py
class ProfileService:
def build_profile(
self,
user: User,
social_accounts: list[SocialAccountInfo],
current_provider: str,
) -> ProfileDTO:
account = self._select_social_account(social_accounts, current_provider)
username = self._resolve_username(user, account)
nickname = self._resolve_nickname(user, account, username)
return ProfileDTO(
username=username,
nickname=nickname,
phone_number=user.phone_number.formatted() if user.phone_number else None,
provider=account.provider if account else current_provider,
last_login_at=account.last_login_at if account else None,
)
Infrastructure Layer
Adapters
| Adapter | Port | 역할 |
|---|---|---|
SqlaUserMapper |
UserGateway |
User CRUD |
SqlaUserCharacterMapper |
UserCharacterGateway |
캐릭터 인벤토리 |
SqlaAuthUserReader |
AuthUserReader |
auth 읽기 전용 |
GrpcCharacterClient |
CharacterClient |
character gRPC 호출 |
# infrastructure/adapters/user_character_mapper_sqla.py
class SqlaUserCharacterMapper(UserCharacterGateway):
async def upsert(self, **kwargs) -> UpsertResult:
stmt = (
insert(UserCharacterModel)
.values(**kwargs)
.on_conflict_do_update(
constraint="uq_user_character_code",
set_={
"character_id": kwargs["character_id"],
"character_name": kwargs["character_name"],
# ...
},
)
.returning(UserCharacterModel)
)
result = await self._session.execute(stmt)
row = result.scalar_one()
return UpsertResult(
was_existing=(row.updated_at != row.acquired_at),
)
# infrastructure/grpc_client/character_client_grpc.py
class GrpcCharacterClient(CharacterClient):
def __init__(self, settings: Settings):
self._circuit_breaker = CircuitBreaker(
name="character-grpc",
fail_max=settings.circuit_fail_max,
timeout_duration=settings.circuit_timeout_duration,
)
async def get_default_character(self) -> DefaultCharacterDTO | None:
try:
return await self._circuit_breaker.call_async(self._call_impl)
except CircuitBreakerError:
logger.warning("Circuit breaker OPEN")
return None
디렉토리 구조
apps/users (API + gRPC Server)
apps/users/
├── main.py # FastAPI + gRPC 서버
├── requirements.txt
├── Dockerfile
│
├── setup/
│ ├── config/settings.py
│ └── dependencies.py # DI Container
│
├── presentation/
│ ├── http/
│ │ ├── controllers/
│ │ │ ├── profile.py # GET/PATCH/DELETE /user/me
│ │ │ └── characters.py # GET /user/me/characters
│ │ └── schemas/
│ │ ├── profile.py
│ │ └── character.py
│ └── grpc/
│ ├── user_character_servicer.py # GrantCharacter RPC
│ └── schemas.py # Proto ↔ DTO
│
├── application/
│ ├── commands/
│ │ ├── update_profile.py
│ │ ├── delete_user.py
│ │ └── grant_character.py
│ ├── queries/
│ │ ├── get_profile.py
│ │ ├── list_characters.py
│ │ └── check_ownership.py
│ └── common/
│ ├── dto/
│ │ ├── profile.py
│ │ └── character.py
│ ├── ports/
│ │ ├── user_gateway.py
│ │ ├── user_character_gateway.py
│ │ ├── auth_user_reader.py
│ │ └── character_client.py
│ └── exceptions/
│
├── domain/
│ ├── entities/
│ │ ├── user.py
│ │ └── user_character.py
│ ├── value_objects/
│ │ ├── phone_number.py
│ │ └── ownership_status.py
│ ├── services/
│ │ └── profile_service.py
│ └── exceptions/
│
├── infrastructure/
│ ├── adapters/
│ │ ├── user_mapper_sqla.py
│ │ ├── user_character_mapper_sqla.py
│ │ └── auth_user_reader_sqla.py
│ ├── grpc_client/
│ │ └── character_client_grpc.py
│ └── persistence_postgres/
│ └── models/
│
└── proto/
├── user_character.proto
└── character/
└── character.proto
apps/users_worker (Celery 배치)
apps/users_worker/
├── main.py # Celery 앱
├── requirements.txt
├── Dockerfile
├── application/
│ └── commands/
│ └── save_character_batch.py # 배치 저장 UseCase
└── infrastructure/
└── persistence_postgres/
└── user_character_bulk_store.py # BULK UPSERT
파일별 매핑 테이블
Services → Commands/Queries
| Legacy | Clean Architecture | 분류 |
|---|---|---|
services/my.py::get_current_user() |
queries/get_profile.py |
Query |
services/my.py::update_current_user() |
commands/update_profile.py |
Command |
services/my.py::delete_current_user() |
commands/delete_user.py |
Command |
services/characters.py::list_owned() |
queries/list_characters.py |
Query |
services/characters.py::owns_character() |
queries/check_ownership.py |
Query |
rpc/v1/user_character_servicer.py::GrantCharacter() |
commands/grant_character.py |
Command |
Domain Logic 추출
| Legacy | Clean Architecture | 역할 |
|---|---|---|
services/my.py::_resolve_username() |
domain/services/profile_service.py |
사용자명 결정 |
services/my.py::_resolve_nickname() |
domain/services/profile_service.py |
닉네임 결정 |
services/my.py::_select_social_account() |
domain/services/profile_service.py |
소셜 계정 선택 |
services/my.py::_normalize_phone_number() |
domain/value_objects/phone_number.py |
전화번호 정규화 |
Repositories → Adapters
| Legacy | Clean Architecture | Port |
|---|---|---|
repositories/user_repository.py |
adapters/user_mapper_sqla.py |
UserGateway |
repositories/user_character_repository.py |
adapters/user_character_mapper_sqla.py |
UserCharacterGateway |
repositories/user_social_account_repository.py |
adapters/auth_user_reader_sqla.py |
AuthUserReader |
gRPC 관련
| Legacy | Clean Architecture | 역할 |
|---|---|---|
rpc/character_client.py |
infrastructure/grpc_client/character_client_grpc.py |
character gRPC 클라이언트 |
rpc/v1/user_character_servicer.py |
presentation/grpc/user_character_servicer.py |
gRPC Servicer |
proto/* |
proto/* |
그대로 유지 |
마이그레이션 단계
Phase 1: Domain Layer
- Entity 정의 (
User,UserCharacter) - Value Object 정의 (
PhoneNumber,OwnershipStatus) - Domain Service 추출 (
ProfileService)
Phase 2: Application Layer
- Port 정의 (
UserGateway,UserCharacterGateway,AuthUserReader,CharacterClient) - Commands 구현 (
UpdateProfile,DeleteUser,GrantCharacter) - Queries 구현 (
GetProfile,ListCharacters,CheckOwnership)
Phase 3: Infrastructure Layer
- SQLAlchemy Adapters 구현
- gRPC Client 구현 (Circuit Breaker 포함)
- ORM Models 분리
Phase 4: Presentation Layer
- HTTP Controllers 구현
- gRPC Servicer 구현
- DI Container 구성 (
dependencies.py)
Phase 5: Worker 분리
apps/users_worker디렉토리 생성- 배치 저장 UseCase 구현
- Dockerfile 및 CI 설정
References
- Clean Architecture #2: Auth Clean Architecture 구현
- Robert C. Martin, "Clean Architecture" (2017)
- Vaughn Vernon, "Implementing Domain-Driven Design" (2013)