···2233## Executive Summary
4455-This document provides a comprehensive implementation plan for sharing types between the backend and frontend using npm workspaces. The approach balances DDD/layered architecture principles with pragmatic simplicity for a startup codebase.
55+This document provides a comprehensive implementation plan for sharing types between the backend and frontend using npm workspaces. **This approach follows proper DDD/layered architecture principles** - it's not a compromise or shortcut.
66+77+### Quick Answer: Why Not Add Mappers?
88+99+**TL;DR**: You already have the important mapper (Domain → Application), and adding another layer (Application → Infrastructure) would just be ceremony when the types are identical.
1010+1111+**You're already doing this (correct DDD):**
1212+```
1313+Domain Entity → Use Case (maps to DTO) → Controller (passes through) → Frontend
1414+ ↓ ↓ ↓
1515+Rich domain model Application Layer DTO Same DTO
1616+```
1717+1818+**The shared types represent the Application Layer**, not Infrastructure. Both backend controllers and frontend clients consume the same Application Layer contract - this is **textbook Ports & Adapters pattern**.
1919+2020+**When you WOULD need mappers:**
2121+- API versioning (supporting v1 and v2)
2222+- Multiple protocols (REST + GraphQL + gRPC)
2323+- Public API (hiding internal structures)
2424+- Different client needs (mobile vs web)
2525+2626+See "When Would You Need Mappers?" section for details.
2727+2828+**Bottom line**: Your architecture is already sound. The `@semble/types` package represents Application Layer contracts that both backend and frontend depend on (correct dependency direction).
629730## Current State Analysis
831···40633. **Infrastructure Layer**: Controllers, Repositories, External Services
41644. **Presentation Layer**: UI Components, API Clients
42654343-### Where Types Belong
6666+### Understanding the Mapping Question
6767+6868+**Your codebase ALREADY does Domain → Application mapping:**
6969+7070+```typescript
7171+// Domain Entity (lives in domain layer)
7272+class Collection {
7373+ collectionId: CollectionId;
7474+ name: CollectionName;
7575+ authorId: UserId;
7676+ description?: CollectionDescription;
7777+ // ... rich domain behavior
7878+}
7979+8080+// Use Case maps Domain → DTO (Application Layer)
8181+async execute(): Promise<Result<CollectionDTO>> {
8282+ const collection = await this.repo.findById(id);
8383+8484+ return {
8585+ id: collection.collectionId.value,
8686+ name: collection.name.toString(),
8787+ author: { ... }, // fetched and mapped
8888+ description: collection.description?.value,
8989+ cardCount: collection.cardCount,
9090+ createdAt: collection.createdAt.toISOString(),
9191+ updatedAt: collection.updatedAt.toISOString(),
9292+ };
9393+}
9494+```
9595+9696+**The question is: Do you need Controller → HTTP Response mapping?**
9797+9898+### Three Approaches Compared
9999+100100+#### Option 1: Pure DDD with Mappers (Maximum Separation)
101101+102102+```typescript
103103+// Domain Layer
104104+class Collection { /* rich domain model */ }
105105+106106+// Application Layer
107107+interface CollectionDTO {
108108+ id: string;
109109+ name: string;
110110+ // ... internal representation
111111+}
112112+113113+// Infrastructure Layer
114114+interface CollectionHttpResponse {
115115+ id: string;
116116+ name: string;
117117+ // ... HTTP API representation
118118+}
119119+120120+// Controller has mapper
121121+async executeImpl(req, res) {
122122+ const result = await this.useCase.execute(query);
123123+ const httpResponse = this.mapDtoToHttpResponse(result.value);
124124+ return this.ok(res, httpResponse);
125125+}
126126+```
127127+128128+**When you need this:**
129129+- ✅ Public API that needs versioning independent of domain
130130+- ✅ Domain model significantly different from API representation
131131+- ✅ Multiple API formats (REST, GraphQL, gRPC) from same domain
132132+- ✅ Need to hide internal domain details from external consumers
133133+- ✅ Large team with separate domain/API teams
134134+135135+**Drawbacks:**
136136+- ❌ High ceremony when DTO ≈ HTTP Response
137137+- ❌ Boilerplate mapper code
138138+- ❌ Slower iteration velocity
139139+140140+#### Option 2: Shared Application Layer Types (Recommended)
141141+142142+```typescript
143143+// Shared Types Package (@semble/types)
144144+// Conceptually lives in Application Layer
145145+interface Collection {
146146+ id: string;
147147+ name: string;
148148+ author: User;
149149+ // ...
150150+}
151151+152152+// Use Case returns Application Layer type
153153+async execute(): Promise<Result<Collection>> {
154154+ // Maps Domain → Application DTO
155155+ return this.mapDomainToDTO(domainCollection);
156156+}
157157+158158+// Controller passes through (no mapping needed)
159159+async executeImpl(req, res) {
160160+ const result = await this.useCase.execute(query);
161161+ return this.ok<Collection>(res, result.value);
162162+}
163163+164164+// Frontend uses same Application Layer type
165165+async getCollection(): Promise<Collection> {
166166+ return this.http.get('/collections/123');
167167+}
168168+```
169169+170170+**When this is appropriate:**
171171+- ✅ Monorepo with tight frontend/backend coupling
172172+- ✅ DTO and HTTP Response are identical (or nearly so)
173173+- ✅ Private/internal API (not public third-party API)
174174+- ✅ Small team that values velocity
175175+- ✅ You're already mapping Domain → DTO properly
176176+177177+**Key insight:** The shared types represent the **Application Layer contract**, not Infrastructure. Both the backend Use Case and frontend are clients of this application layer contract.
178178+179179+**Drawbacks:**
180180+- ⚠️ Harder to version API independently
181181+- ⚠️ Frontend sees application layer types (but this may be fine)
182182+183183+#### Option 3: Infrastructure-Owned Types with Reverse Mapping (Anti-Pattern)
184184+185185+```typescript
186186+// Infrastructure defines types
187187+interface CollectionHttpResponse { /* ... */ }
188188+189189+// Use Case returns infrastructure type (WRONG!)
190190+async execute(): Promise<Result<CollectionHttpResponse>> {
191191+ // Use case now depends on infrastructure layer
192192+}
193193+```
194194+195195+**This is an anti-pattern** because:
196196+- ❌ Use Cases depend on Infrastructure (breaks DDD layering)
197197+- ❌ Application layer coupled to HTTP representation
198198+- ❌ Can't reuse Use Cases for non-HTTP interfaces
199199+200200+### Our Recommendation: Option 2 (Shared Application Types)
201201+202202+**Why this is the right choice for your codebase:**
203203+204204+1. **You're already mapping Domain → Application:**
205205+ ```typescript
206206+ // GetCollectionsForUrlUseCase.ts - lines 122-151
207207+ const enrichedCollections: CollectionDTO[] = await Promise.all(
208208+ result.items.map(async (item) => {
209209+ // Fetching related data
210210+ const author = profileMap.get(item.authorId);
211211+ const collection = await this.collectionRepo.findById(...);
212212+213213+ // Mapping domain to DTO
214214+ return {
215215+ id: item.id,
216216+ uri: item.uri,
217217+ name: item.name,
218218+ description: item.description,
219219+ author, // enriched
220220+ cardCount: collection.cardCount,
221221+ createdAt: collection.createdAt.toISOString(),
222222+ updatedAt: collection.updatedAt.toISOString(),
223223+ };
224224+ })
225225+ );
226226+ ```
227227+ This is **proper DDD** - the Use Case orchestrates domain objects and produces DTOs.
228228+229229+2. **Your DTOs ARE your HTTP responses:**
230230+ - Backend `CollectionDTO` is identical to frontend `Collection`
231231+ - No transformation happens in controllers
232232+ - Controllers just pass through the DTO
233233+234234+3. **The shared types represent Application Layer, not Infrastructure:**
235235+ - Think of it as: "The Application Layer contract that both backend and frontend implement"
236236+ - Backend: Use Cases produce this contract
237237+ - Frontend: API Client consumes this contract
238238+ - Infrastructure (controllers): Just pipes the data through
239239+240240+4. **You maintain proper boundaries:**
241241+ ```
242242+ Domain Layer (Rich Models)
243243+ ↓
244244+ Application Layer (DTOs - Shared Types)
245245+ ↓
246246+ Infrastructure (HTTP) ← → Frontend (HTTP Client)
247247+ ```
248248+249249+### When Would You Need Mappers?
250250+251251+Add a separate HTTP Response layer if:
252252+253253+1. **API Versioning:**
254254+ ```typescript
255255+ // v1: { id, name }
256256+ // v2: { id, title } // renamed field
442574545-**Traditional DDD Approach:**
4646-- DTOs live in **Application Layer** (Use Case outputs)
4747-- DTOs define contract between Use Case and Controller
4848-- DTOs should NOT be directly exposed to external consumers
4949-- Controllers map DTOs to HTTP response formats
258258+ // Use mapper to support both versions
259259+ function mapToV1(dto: CollectionDTO): CollectionV1Response {
260260+ return { id: dto.id, name: dto.name };
261261+ }
262262+ ```
502635151-**Pragmatic Reality for Startups:**
5252-- Maintaining separate DTO and HTTP response types is overhead
5353-- For simple CRUD APIs, DTOs ≈ HTTP responses is acceptable
5454-- The key is being **intentional** about this choice
264264+2. **Different Representations:**
265265+ ```typescript
266266+ // REST API: flat structure
267267+ { collectionId: '123', authorId: '456' }
552685656-### Our Approach: Pragmatic DDD
269269+ // GraphQL: nested structure
270270+ { id: '123', author: { id: '456', name: '...' } }
271271+ ```
572725858-We'll create **shared API contract types** that serve dual purposes:
5959-1. Use Case output types (replacing current DTOs)
6060-2. HTTP API contract types (replacing frontend response types)
273273+3. **Hide Internal Details:**
274274+ ```typescript
275275+ // DTO has internal fields
276276+ interface CollectionDTO {
277277+ id: string;
278278+ name: string;
279279+ internalAuditLog: AuditEntry[]; // don't expose
280280+ }
612816262-This is a pragmatic simplification that:
6363-- ✅ Eliminates duplication
6464-- ✅ Maintains type safety
6565-- ✅ Keeps code simple
6666-- ⚠️ Slightly blurs application/infrastructure boundary (acceptable tradeoff)
282282+ // HTTP response filters
283283+ interface CollectionResponse {
284284+ id: string;
285285+ name: string;
286286+ // no audit log
287287+ }
288288+ ```
289289+290290+4. **Multiple Clients with Different Needs:**
291291+ - Mobile app needs smaller payloads
292292+ - Admin dashboard needs more details
293293+ - Public API needs sanitized data
294294+295295+### Implementation Strategy
296296+297297+**Organize shared types by architectural layer:**
298298+299299+```typescript
300300+// src/types/src/api/
301301+// These are Application Layer DTOs, not Infrastructure types
302302+303303+// common.ts - Core domain concepts
304304+export interface User { /* ... */ }
305305+export interface Collection { /* ... */ }
306306+307307+// requests.ts - Use Case inputs
308308+export interface GetCollectionsParams { /* ... */ }
309309+310310+// responses.ts - Use Case outputs
311311+export interface GetCollectionsResponse { /* ... */ }
312312+```
313313+314314+**Use Cases depend on these types:**
315315+```typescript
316316+import { GetCollectionsResponse } from '@semble/types';
317317+318318+class GetCollectionsUseCase {
319319+ async execute(params): Promise<Result<GetCollectionsResponse>> {
320320+ // Maps Domain → Application DTO
321321+ }
322322+}
323323+```
324324+325325+**Controllers are thin pipes:**
326326+```typescript
327327+import { GetCollectionsResponse } from '@semble/types';
328328+329329+class GetCollectionsController {
330330+ async executeImpl(req, res) {
331331+ const result = await this.useCase.execute(params);
332332+ return this.ok<GetCollectionsResponse>(res, result.value);
333333+ }
334334+}
335335+```
336336+337337+**Frontend consumes Application Layer contract:**
338338+```typescript
339339+import { GetCollectionsResponse } from '@semble/types';
340340+341341+class ApiClient {
342342+ async getCollections(): Promise<GetCollectionsResponse> {
343343+ return this.http.get('/collections');
344344+ }
345345+}
346346+```
347347+348348+### Summary
349349+350350+**What we're doing:**
351351+- ✅ Sharing **Application Layer** types between backend and frontend
352352+- ✅ Use Cases map Domain → Application DTO (proper DDD)
353353+- ✅ Controllers validate HTTP and pass through DTOs
354354+- ✅ Single source of truth for application contracts
355355+356356+**What we're NOT doing:**
357357+- ❌ Skipping Domain → Application mapping (we do this!)
358358+- ❌ Letting infrastructure dictate application types
359359+- ❌ Breaking DDD layer dependencies
360360+361361+**This is pragmatic DDD because:**
362362+- Domain layer remains pure ✅
363363+- Application layer defines contracts ✅
364364+- Infrastructure depends on Application (correct direction) ✅
365365+- Frontend and Backend share Application contracts (pragmatic) ✅
366366+367367+The key realization: In a monorepo where frontend and backend deploy together, **sharing Application Layer types is not a DDD violation** - it's recognizing that both are clients of the same application layer.
6736868369## Validation Strategy in DDD
69370···70210037031004### What We're NOT Doing (And Why That's OK)
7041005705705-❌ **Separate DTOs and API types**:
706706-- Reason: For this codebase, they're effectively the same
707707-- Tradeoff: Slightly impure DDD, but significantly simpler
10061006+❌ **Separate DTO and HTTP Response mapping layer**:
10071007+- Reason: Application DTOs and HTTP responses are identical
10081008+- Reality: Controllers would just be identity mappers (pointless ceremony)
10091009+- When to add: If you need API versioning, different client representations, or hide internal fields
10101010+- See "When Would You Need Mappers?" section above for specific scenarios
7081011709709-❌ **Runtime validation of responses**:
710710-- Reason: TypeScript compilation guarantees response shape
711711-- Tradeoff: Could add Zod validation of Use Case outputs, but not needed initially
10121012+❌ **Runtime validation of Use Case outputs**:
10131013+- Reason: TypeScript compilation guarantees response shape from Use Cases
10141014+- Alternative: Could add Zod validation of DTO construction, but not needed initially
10151015+- When to add: If you have bugs where Use Cases return malformed DTOs
71210167131017❌ **OpenAPI schema generation**:
714714-- Reason: Can add later if needed
715715-- Benefit: Shared types make this easier in the future
10181018+- Reason: Can add later if needed (Zod schemas make this easy)
10191019+- Benefit: Shared types + Zod schemas = future OpenAPI generation is trivial
10201020+- When to add: When you want API documentation, client SDK generation, or contract testing
71610217171022❌ **Separate versioning of types package**:
718718-- Reason: Monorepo with synchronized deploys
719719-- Note: Could add semantic versioning later if needed
10231023+- Reason: Monorepo with synchronized deploys (frontend/backend always in sync)
10241024+- When to add: If you publish a public API or have multiple clients on different versions
10251025+- Note: For now, Git commits provide version history
10261026+10271027+❌ **Multiple API representations (REST, GraphQL, gRPC)**:
10281028+- Reason: Only building REST API currently
10291029+- When to add: When you need GraphQL, gRPC, or other protocols (then add mappers)
72010307211031### What Makes This "Barebones But Reliable"
7221032723723-✅ **Single source of truth**: Types defined once, used everywhere
724724-✅ **Compile-time safety**: TypeScript catches mismatches immediately
725725-✅ **Runtime validation**: Zod at boundaries catches bad requests
726726-✅ **Simple workflow**: Edit types, both sides update automatically
727727-✅ **Standard tooling**: npm workspaces (not custom build scripts)
10331033+✅ **Proper DDD boundaries**: Domain → Application mapping happens in Use Cases
10341034+✅ **Single source of truth**: Application Layer types defined once in `@semble/types`
10351035+✅ **Compile-time safety**: TypeScript catches type mismatches immediately
10361036+✅ **Runtime validation**: Zod at controller boundaries validates incoming requests
10371037+✅ **Three-tier validation**: Infrastructure (HTTP) → Application (business rules) → Domain (invariants)
10381038+✅ **Simple workflow**: Edit shared types, both backend and frontend update automatically
10391039+✅ **Standard tooling**: npm workspaces, TypeScript, Zod (no custom tooling)
10401040+✅ **Refactor-friendly**: Easy to add mapping layer later if needed
10411041+10421042+### What Makes This "Good DDD"
10431043+10441044+✅ **Domain layer remains pure**: No external dependencies, rich behavior
10451045+✅ **Application layer orchestrates**: Use Cases compose domain objects, return DTOs
10461046+✅ **Infrastructure depends on Application**: Controllers use Application types (correct direction)
10471047+✅ **Separation of concerns**: Domain logic, application logic, HTTP transport all separated
10481048+✅ **Validation hierarchy**: Each layer validates at its level of abstraction
10491049+✅ **Type safety across boundaries**: Shared Application contracts prevent drift
10501050+10511051+The key insight: **Sharing Application Layer types between backend and frontend is not a DDD violation** when:
10521052+1. Both are clients of the same Application Layer
10531053+2. They deploy together (monorepo)
10541054+3. The Application DTOs appropriately abstract the Domain Model
72810557291056## Troubleshooting
7301057···77210997731100## DDD Architecture Notes
7741101775775-### What We Preserved
776776-- ✅ Domain layer remains pure (no external dependencies)
777777-- ✅ Use cases validate using domain value objects
778778-- ✅ Domain-driven validation hierarchy (domain → application → infrastructure)
779779-- ✅ Clear boundaries between layers
11021102+### What We Preserved (Proper DDD)
11031103+11041104+✅ **Domain Layer Purity**:
11051105+- Domain entities, value objects, and domain services have no external dependencies
11061106+- Domain models encapsulate business logic and invariants
11071107+- Domain layer doesn't know about HTTP, DTOs, or frontend
11081108+11091109+✅ **Application Layer Orchestration**:
11101110+- Use Cases orchestrate domain objects to fulfill application requirements
11111111+- Use Cases map rich domain models → simple DTOs (proper anti-corruption layer)
11121112+- DTOs hide domain complexity from external consumers
11131113+- Application layer defines the contract for consumers (backend and frontend)
11141114+11151115+✅ **Infrastructure Layer Dependency Direction**:
11161116+- Controllers depend on Application types (correct: Infrastructure → Application)
11171117+- Controllers do NOT define types that Application depends on (would be wrong)
11181118+- Infrastructure implements Application contracts, not the other way around
11191119+11201120+✅ **Three-Tier Validation Hierarchy**:
11211121+- **Domain**: Invariants enforced in value objects and entities
11221122+- **Application**: Business rules validated in use cases
11231123+- **Infrastructure**: HTTP request structure validated in controllers
11241124+11251125+✅ **Separation of Concerns**:
11261126+- Domain logic in entities/value objects
11271127+- Application logic in use cases
11281128+- HTTP transport details in controllers
11291129+- Each layer has clear responsibilities
11301130+11311131+### What's Actually Happening (Not a Compromise)
11321132+11331133+✅ **Shared Application Layer Types**:
11341134+- `@semble/types` represents the **Application Layer contract**
11351135+- Backend Use Cases produce these contracts
11361136+- Frontend API Client consumes these contracts
11371137+- Both are clients of the Application Layer (this is correct DDD!)
11381138+11391139+✅ **Controllers as Thin Adapters**:
11401140+- Controllers adapt HTTP → Application Layer (parse request, call use case)
11411141+- Controllers adapt Application Layer → HTTP (serialize DTO to JSON)
11421142+- No additional mapping needed when DTO shape = HTTP response shape
11431143+- This is a **valid DDD pattern** (Hexagonal Architecture adapter)
11441144+11451145+### Why This Is Good DDD (Not Just "OK for Startups")
11461146+11471147+**From Eric Evans / Martin Fowler:**
11481148+- DTOs exist to cross architectural boundaries ✅ (we do this)
11491149+- Application Layer should be independent of delivery mechanism ✅ (it is)
11501150+- Infrastructure should depend on Application, not vice versa ✅ (correct)
11511151+- Shared kernel is acceptable when bounded contexts align ✅ (monorepo, single app)
11521152+11531153+**From Hexagonal Architecture (Ports & Adapters):**
11541154+- Application Layer defines "ports" (interfaces/contracts) ✅ (`@semble/types`)
11551155+- Infrastructure provides "adapters" (controllers, API clients) ✅ (thin HTTP adapters)
11561156+- Multiple adapters can implement same port ✅ (backend controller, frontend client)
11571157+- This is the **textbook pattern**!
11581158+11591159+**When you'd need an additional mapping layer:**
11601160+1. **API Versioning**: Supporting v1 and v2 simultaneously
11611161+2. **Different Protocols**: REST + GraphQL + gRPC from same Application Layer
11621162+3. **Public API**: Need to hide internal structures
11631163+4. **Legacy Migration**: Gradual transition between representations
11641164+11651165+For a monorepo with single API version and private API, shared Application types are **architecturally sound**.
11661166+11671167+### Dependency Flow (Correct DDD)
11681168+11691169+```
11701170+┌─────────────────────────────────────────────────────┐
11711171+│ Domain Layer │
11721172+│ - Entities (Collection, Card) │
11731173+│ - Value Objects (CollectionId, URL) │
11741174+│ - Domain Services │
11751175+│ No dependencies ↓ │
11761176+└─────────────────────────────────────────────────────┘
11771177+ ↓
11781178+┌─────────────────────────────────────────────────────┐
11791179+│ Application Layer (@semble/types) │
11801180+│ - Use Cases │
11811181+│ - DTOs (Collection, GetCollectionsResponse) │
11821182+│ - Defines contracts for external consumers │
11831183+│ Depends on: Domain ↑ │
11841184+└─────────────────────────────────────────────────────┘
11851185+ ↓ ↓
11861186+┌──────────────────────┐ ┌──────────────────────┐
11871187+│ Infrastructure Layer │ │ Presentation Layer │
11881188+│ - Controllers │ │ - API Client │
11891189+│ - HTTP Adapters │ │ - Frontend │
11901190+│ Depends on: App ↑ │ │ Depends on: App ↑ │
11911191+└──────────────────────┘ └──────────────────────┘
11921192+```
11931193+11941194+**Key point**: Frontend is a **client of the Application Layer**, not the Infrastructure Layer. It happens to communicate via HTTP, but it depends on the same Application contracts as the backend controllers.
11951195+11961196+### The Real Question: Are Your DTOs Well-Designed?
11971197+11981198+The quality of this architecture depends on whether your DTOs properly abstract the domain:
11991199+12001200+✅ **Good DTO Design** (what you have):
12011201+```typescript
12021202+// Domain: Rich model with behavior
12031203+class Collection {
12041204+ collectionId: CollectionId; // Value object
12051205+ name: CollectionName; // Value object with validation
12061206+ authorId: UserId; // Value object
12071207+ description?: CollectionDescription; // Value object
12081208+ cardIds: CardId[]; // List of value objects
12091209+ addCard(cardId: CardId) { /* logic */ }
12101210+ removeCard(cardId: CardId) { /* logic */ }
12111211+}
12121212+12131213+// DTO: Simple data structure for transfer
12141214+interface Collection {
12151215+ id: string; // Unwrapped value
12161216+ name: string; // Unwrapped value
12171217+ author: User; // Enriched relationship
12181218+ description?: string; // Unwrapped value
12191219+ cardCount: number; // Computed property
12201220+ createdAt: string; // ISO string
12211221+ updatedAt: string; // ISO string
12221222+}
12231223+```
12241224+12251225+This is **good separation**:
12261226+- Domain has rich behavior, validation, invariants
12271227+- DTO is simple, serializable, consumer-friendly
12281228+- Use Case does the mapping (proper layer responsibility)
12291229+12301230+❌ **Bad DTO Design** (anti-pattern):
12311231+```typescript
12321232+// Exposing domain internals
12331233+interface CollectionDTO {
12341234+ collectionId: CollectionId; // WRONG: exposing domain value object
12351235+ _aggregateVersion: number; // WRONG: exposing internal details
12361236+ domainEvents: DomainEvent[]; // WRONG: leaking domain events
12371237+}
12381238+```
7801239781781-### Pragmatic Compromises
782782-- ⚠️ Shared types blur application/infrastructure boundary
783783-- ⚠️ Use cases return types designed for HTTP responses
784784-- ✅ But: This is **intentional** and **documented**
12401240+### Summary: This IS Good DDD
7851241786786-### Why This Is OK for a Startup
787787-1. **Velocity**: Eliminates significant boilerplate
788788-2. **Type Safety**: Maintains compile-time guarantees
789789-3. **Maintainability**: Single source of truth reduces bugs
790790-4. **Scalability**: Can refactor later if needed (types are internal contract)
12421242+- ✅ Domain → Application → Infrastructure (correct dependency flow)
12431243+- ✅ Use Cases map Domain → DTO (proper anti-corruption layer)
12441244+- ✅ DTOs abstract domain complexity (proper encapsulation)
12451245+- ✅ Multiple adapters consume same Application contract (Ports & Adapters)
12461246+- ✅ Shared Application types in monorepo (Shared Kernel pattern)
7911247792792-The key is being **intentional** about the tradeoff rather than accidentally coupling layers.
12481248+**This is not a compromise or shortcut** - it's a well-architected system following established DDD patterns. The key is that you're sharing **Application Layer types**, not Infrastructure types, and the dependency arrows point in the correct direction.
79312497941250---
7951251