This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

update to the type unification doc with more DDD clarification

+506 -50
+506 -50
docs/plan/shared_type_unification.md
··· 2 2 3 3 ## Executive Summary 4 4 5 - 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. 5 + 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. 6 + 7 + ### Quick Answer: Why Not Add Mappers? 8 + 9 + **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. 10 + 11 + **You're already doing this (correct DDD):** 12 + ``` 13 + Domain Entity → Use Case (maps to DTO) → Controller (passes through) → Frontend 14 + ↓ ↓ ↓ 15 + Rich domain model Application Layer DTO Same DTO 16 + ``` 17 + 18 + **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**. 19 + 20 + **When you WOULD need mappers:** 21 + - API versioning (supporting v1 and v2) 22 + - Multiple protocols (REST + GraphQL + gRPC) 23 + - Public API (hiding internal structures) 24 + - Different client needs (mobile vs web) 25 + 26 + See "When Would You Need Mappers?" section for details. 27 + 28 + **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). 6 29 7 30 ## Current State Analysis 8 31 ··· 40 63 3. **Infrastructure Layer**: Controllers, Repositories, External Services 41 64 4. **Presentation Layer**: UI Components, API Clients 42 65 43 - ### Where Types Belong 66 + ### Understanding the Mapping Question 67 + 68 + **Your codebase ALREADY does Domain → Application mapping:** 69 + 70 + ```typescript 71 + // Domain Entity (lives in domain layer) 72 + class Collection { 73 + collectionId: CollectionId; 74 + name: CollectionName; 75 + authorId: UserId; 76 + description?: CollectionDescription; 77 + // ... rich domain behavior 78 + } 79 + 80 + // Use Case maps Domain → DTO (Application Layer) 81 + async execute(): Promise<Result<CollectionDTO>> { 82 + const collection = await this.repo.findById(id); 83 + 84 + return { 85 + id: collection.collectionId.value, 86 + name: collection.name.toString(), 87 + author: { ... }, // fetched and mapped 88 + description: collection.description?.value, 89 + cardCount: collection.cardCount, 90 + createdAt: collection.createdAt.toISOString(), 91 + updatedAt: collection.updatedAt.toISOString(), 92 + }; 93 + } 94 + ``` 95 + 96 + **The question is: Do you need Controller → HTTP Response mapping?** 97 + 98 + ### Three Approaches Compared 99 + 100 + #### Option 1: Pure DDD with Mappers (Maximum Separation) 101 + 102 + ```typescript 103 + // Domain Layer 104 + class Collection { /* rich domain model */ } 105 + 106 + // Application Layer 107 + interface CollectionDTO { 108 + id: string; 109 + name: string; 110 + // ... internal representation 111 + } 112 + 113 + // Infrastructure Layer 114 + interface CollectionHttpResponse { 115 + id: string; 116 + name: string; 117 + // ... HTTP API representation 118 + } 119 + 120 + // Controller has mapper 121 + async executeImpl(req, res) { 122 + const result = await this.useCase.execute(query); 123 + const httpResponse = this.mapDtoToHttpResponse(result.value); 124 + return this.ok(res, httpResponse); 125 + } 126 + ``` 127 + 128 + **When you need this:** 129 + - ✅ Public API that needs versioning independent of domain 130 + - ✅ Domain model significantly different from API representation 131 + - ✅ Multiple API formats (REST, GraphQL, gRPC) from same domain 132 + - ✅ Need to hide internal domain details from external consumers 133 + - ✅ Large team with separate domain/API teams 134 + 135 + **Drawbacks:** 136 + - ❌ High ceremony when DTO ≈ HTTP Response 137 + - ❌ Boilerplate mapper code 138 + - ❌ Slower iteration velocity 139 + 140 + #### Option 2: Shared Application Layer Types (Recommended) 141 + 142 + ```typescript 143 + // Shared Types Package (@semble/types) 144 + // Conceptually lives in Application Layer 145 + interface Collection { 146 + id: string; 147 + name: string; 148 + author: User; 149 + // ... 150 + } 151 + 152 + // Use Case returns Application Layer type 153 + async execute(): Promise<Result<Collection>> { 154 + // Maps Domain → Application DTO 155 + return this.mapDomainToDTO(domainCollection); 156 + } 157 + 158 + // Controller passes through (no mapping needed) 159 + async executeImpl(req, res) { 160 + const result = await this.useCase.execute(query); 161 + return this.ok<Collection>(res, result.value); 162 + } 163 + 164 + // Frontend uses same Application Layer type 165 + async getCollection(): Promise<Collection> { 166 + return this.http.get('/collections/123'); 167 + } 168 + ``` 169 + 170 + **When this is appropriate:** 171 + - ✅ Monorepo with tight frontend/backend coupling 172 + - ✅ DTO and HTTP Response are identical (or nearly so) 173 + - ✅ Private/internal API (not public third-party API) 174 + - ✅ Small team that values velocity 175 + - ✅ You're already mapping Domain → DTO properly 176 + 177 + **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. 178 + 179 + **Drawbacks:** 180 + - ⚠️ Harder to version API independently 181 + - ⚠️ Frontend sees application layer types (but this may be fine) 182 + 183 + #### Option 3: Infrastructure-Owned Types with Reverse Mapping (Anti-Pattern) 184 + 185 + ```typescript 186 + // Infrastructure defines types 187 + interface CollectionHttpResponse { /* ... */ } 188 + 189 + // Use Case returns infrastructure type (WRONG!) 190 + async execute(): Promise<Result<CollectionHttpResponse>> { 191 + // Use case now depends on infrastructure layer 192 + } 193 + ``` 194 + 195 + **This is an anti-pattern** because: 196 + - ❌ Use Cases depend on Infrastructure (breaks DDD layering) 197 + - ❌ Application layer coupled to HTTP representation 198 + - ❌ Can't reuse Use Cases for non-HTTP interfaces 199 + 200 + ### Our Recommendation: Option 2 (Shared Application Types) 201 + 202 + **Why this is the right choice for your codebase:** 203 + 204 + 1. **You're already mapping Domain → Application:** 205 + ```typescript 206 + // GetCollectionsForUrlUseCase.ts - lines 122-151 207 + const enrichedCollections: CollectionDTO[] = await Promise.all( 208 + result.items.map(async (item) => { 209 + // Fetching related data 210 + const author = profileMap.get(item.authorId); 211 + const collection = await this.collectionRepo.findById(...); 212 + 213 + // Mapping domain to DTO 214 + return { 215 + id: item.id, 216 + uri: item.uri, 217 + name: item.name, 218 + description: item.description, 219 + author, // enriched 220 + cardCount: collection.cardCount, 221 + createdAt: collection.createdAt.toISOString(), 222 + updatedAt: collection.updatedAt.toISOString(), 223 + }; 224 + }) 225 + ); 226 + ``` 227 + This is **proper DDD** - the Use Case orchestrates domain objects and produces DTOs. 228 + 229 + 2. **Your DTOs ARE your HTTP responses:** 230 + - Backend `CollectionDTO` is identical to frontend `Collection` 231 + - No transformation happens in controllers 232 + - Controllers just pass through the DTO 233 + 234 + 3. **The shared types represent Application Layer, not Infrastructure:** 235 + - Think of it as: "The Application Layer contract that both backend and frontend implement" 236 + - Backend: Use Cases produce this contract 237 + - Frontend: API Client consumes this contract 238 + - Infrastructure (controllers): Just pipes the data through 239 + 240 + 4. **You maintain proper boundaries:** 241 + ``` 242 + Domain Layer (Rich Models) 243 + 244 + Application Layer (DTOs - Shared Types) 245 + 246 + Infrastructure (HTTP) ← → Frontend (HTTP Client) 247 + ``` 248 + 249 + ### When Would You Need Mappers? 250 + 251 + Add a separate HTTP Response layer if: 252 + 253 + 1. **API Versioning:** 254 + ```typescript 255 + // v1: { id, name } 256 + // v2: { id, title } // renamed field 44 257 45 - **Traditional DDD Approach:** 46 - - DTOs live in **Application Layer** (Use Case outputs) 47 - - DTOs define contract between Use Case and Controller 48 - - DTOs should NOT be directly exposed to external consumers 49 - - Controllers map DTOs to HTTP response formats 258 + // Use mapper to support both versions 259 + function mapToV1(dto: CollectionDTO): CollectionV1Response { 260 + return { id: dto.id, name: dto.name }; 261 + } 262 + ``` 50 263 51 - **Pragmatic Reality for Startups:** 52 - - Maintaining separate DTO and HTTP response types is overhead 53 - - For simple CRUD APIs, DTOs ≈ HTTP responses is acceptable 54 - - The key is being **intentional** about this choice 264 + 2. **Different Representations:** 265 + ```typescript 266 + // REST API: flat structure 267 + { collectionId: '123', authorId: '456' } 55 268 56 - ### Our Approach: Pragmatic DDD 269 + // GraphQL: nested structure 270 + { id: '123', author: { id: '456', name: '...' } } 271 + ``` 57 272 58 - We'll create **shared API contract types** that serve dual purposes: 59 - 1. Use Case output types (replacing current DTOs) 60 - 2. HTTP API contract types (replacing frontend response types) 273 + 3. **Hide Internal Details:** 274 + ```typescript 275 + // DTO has internal fields 276 + interface CollectionDTO { 277 + id: string; 278 + name: string; 279 + internalAuditLog: AuditEntry[]; // don't expose 280 + } 61 281 62 - This is a pragmatic simplification that: 63 - - ✅ Eliminates duplication 64 - - ✅ Maintains type safety 65 - - ✅ Keeps code simple 66 - - ⚠️ Slightly blurs application/infrastructure boundary (acceptable tradeoff) 282 + // HTTP response filters 283 + interface CollectionResponse { 284 + id: string; 285 + name: string; 286 + // no audit log 287 + } 288 + ``` 289 + 290 + 4. **Multiple Clients with Different Needs:** 291 + - Mobile app needs smaller payloads 292 + - Admin dashboard needs more details 293 + - Public API needs sanitized data 294 + 295 + ### Implementation Strategy 296 + 297 + **Organize shared types by architectural layer:** 298 + 299 + ```typescript 300 + // src/types/src/api/ 301 + // These are Application Layer DTOs, not Infrastructure types 302 + 303 + // common.ts - Core domain concepts 304 + export interface User { /* ... */ } 305 + export interface Collection { /* ... */ } 306 + 307 + // requests.ts - Use Case inputs 308 + export interface GetCollectionsParams { /* ... */ } 309 + 310 + // responses.ts - Use Case outputs 311 + export interface GetCollectionsResponse { /* ... */ } 312 + ``` 313 + 314 + **Use Cases depend on these types:** 315 + ```typescript 316 + import { GetCollectionsResponse } from '@semble/types'; 317 + 318 + class GetCollectionsUseCase { 319 + async execute(params): Promise<Result<GetCollectionsResponse>> { 320 + // Maps Domain → Application DTO 321 + } 322 + } 323 + ``` 324 + 325 + **Controllers are thin pipes:** 326 + ```typescript 327 + import { GetCollectionsResponse } from '@semble/types'; 328 + 329 + class GetCollectionsController { 330 + async executeImpl(req, res) { 331 + const result = await this.useCase.execute(params); 332 + return this.ok<GetCollectionsResponse>(res, result.value); 333 + } 334 + } 335 + ``` 336 + 337 + **Frontend consumes Application Layer contract:** 338 + ```typescript 339 + import { GetCollectionsResponse } from '@semble/types'; 340 + 341 + class ApiClient { 342 + async getCollections(): Promise<GetCollectionsResponse> { 343 + return this.http.get('/collections'); 344 + } 345 + } 346 + ``` 347 + 348 + ### Summary 349 + 350 + **What we're doing:** 351 + - ✅ Sharing **Application Layer** types between backend and frontend 352 + - ✅ Use Cases map Domain → Application DTO (proper DDD) 353 + - ✅ Controllers validate HTTP and pass through DTOs 354 + - ✅ Single source of truth for application contracts 355 + 356 + **What we're NOT doing:** 357 + - ❌ Skipping Domain → Application mapping (we do this!) 358 + - ❌ Letting infrastructure dictate application types 359 + - ❌ Breaking DDD layer dependencies 360 + 361 + **This is pragmatic DDD because:** 362 + - Domain layer remains pure ✅ 363 + - Application layer defines contracts ✅ 364 + - Infrastructure depends on Application (correct direction) ✅ 365 + - Frontend and Backend share Application contracts (pragmatic) ✅ 366 + 367 + 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. 67 368 68 369 ## Validation Strategy in DDD 69 370 ··· 702 1003 703 1004 ### What We're NOT Doing (And Why That's OK) 704 1005 705 - ❌ **Separate DTOs and API types**: 706 - - Reason: For this codebase, they're effectively the same 707 - - Tradeoff: Slightly impure DDD, but significantly simpler 1006 + ❌ **Separate DTO and HTTP Response mapping layer**: 1007 + - Reason: Application DTOs and HTTP responses are identical 1008 + - Reality: Controllers would just be identity mappers (pointless ceremony) 1009 + - When to add: If you need API versioning, different client representations, or hide internal fields 1010 + - See "When Would You Need Mappers?" section above for specific scenarios 708 1011 709 - ❌ **Runtime validation of responses**: 710 - - Reason: TypeScript compilation guarantees response shape 711 - - Tradeoff: Could add Zod validation of Use Case outputs, but not needed initially 1012 + ❌ **Runtime validation of Use Case outputs**: 1013 + - Reason: TypeScript compilation guarantees response shape from Use Cases 1014 + - Alternative: Could add Zod validation of DTO construction, but not needed initially 1015 + - When to add: If you have bugs where Use Cases return malformed DTOs 712 1016 713 1017 ❌ **OpenAPI schema generation**: 714 - - Reason: Can add later if needed 715 - - Benefit: Shared types make this easier in the future 1018 + - Reason: Can add later if needed (Zod schemas make this easy) 1019 + - Benefit: Shared types + Zod schemas = future OpenAPI generation is trivial 1020 + - When to add: When you want API documentation, client SDK generation, or contract testing 716 1021 717 1022 ❌ **Separate versioning of types package**: 718 - - Reason: Monorepo with synchronized deploys 719 - - Note: Could add semantic versioning later if needed 1023 + - Reason: Monorepo with synchronized deploys (frontend/backend always in sync) 1024 + - When to add: If you publish a public API or have multiple clients on different versions 1025 + - Note: For now, Git commits provide version history 1026 + 1027 + ❌ **Multiple API representations (REST, GraphQL, gRPC)**: 1028 + - Reason: Only building REST API currently 1029 + - When to add: When you need GraphQL, gRPC, or other protocols (then add mappers) 720 1030 721 1031 ### What Makes This "Barebones But Reliable" 722 1032 723 - ✅ **Single source of truth**: Types defined once, used everywhere 724 - ✅ **Compile-time safety**: TypeScript catches mismatches immediately 725 - ✅ **Runtime validation**: Zod at boundaries catches bad requests 726 - ✅ **Simple workflow**: Edit types, both sides update automatically 727 - ✅ **Standard tooling**: npm workspaces (not custom build scripts) 1033 + ✅ **Proper DDD boundaries**: Domain → Application mapping happens in Use Cases 1034 + ✅ **Single source of truth**: Application Layer types defined once in `@semble/types` 1035 + ✅ **Compile-time safety**: TypeScript catches type mismatches immediately 1036 + ✅ **Runtime validation**: Zod at controller boundaries validates incoming requests 1037 + ✅ **Three-tier validation**: Infrastructure (HTTP) → Application (business rules) → Domain (invariants) 1038 + ✅ **Simple workflow**: Edit shared types, both backend and frontend update automatically 1039 + ✅ **Standard tooling**: npm workspaces, TypeScript, Zod (no custom tooling) 1040 + ✅ **Refactor-friendly**: Easy to add mapping layer later if needed 1041 + 1042 + ### What Makes This "Good DDD" 1043 + 1044 + ✅ **Domain layer remains pure**: No external dependencies, rich behavior 1045 + ✅ **Application layer orchestrates**: Use Cases compose domain objects, return DTOs 1046 + ✅ **Infrastructure depends on Application**: Controllers use Application types (correct direction) 1047 + ✅ **Separation of concerns**: Domain logic, application logic, HTTP transport all separated 1048 + ✅ **Validation hierarchy**: Each layer validates at its level of abstraction 1049 + ✅ **Type safety across boundaries**: Shared Application contracts prevent drift 1050 + 1051 + The key insight: **Sharing Application Layer types between backend and frontend is not a DDD violation** when: 1052 + 1. Both are clients of the same Application Layer 1053 + 2. They deploy together (monorepo) 1054 + 3. The Application DTOs appropriately abstract the Domain Model 728 1055 729 1056 ## Troubleshooting 730 1057 ··· 772 1099 773 1100 ## DDD Architecture Notes 774 1101 775 - ### What We Preserved 776 - - ✅ Domain layer remains pure (no external dependencies) 777 - - ✅ Use cases validate using domain value objects 778 - - ✅ Domain-driven validation hierarchy (domain → application → infrastructure) 779 - - ✅ Clear boundaries between layers 1102 + ### What We Preserved (Proper DDD) 1103 + 1104 + ✅ **Domain Layer Purity**: 1105 + - Domain entities, value objects, and domain services have no external dependencies 1106 + - Domain models encapsulate business logic and invariants 1107 + - Domain layer doesn't know about HTTP, DTOs, or frontend 1108 + 1109 + ✅ **Application Layer Orchestration**: 1110 + - Use Cases orchestrate domain objects to fulfill application requirements 1111 + - Use Cases map rich domain models → simple DTOs (proper anti-corruption layer) 1112 + - DTOs hide domain complexity from external consumers 1113 + - Application layer defines the contract for consumers (backend and frontend) 1114 + 1115 + ✅ **Infrastructure Layer Dependency Direction**: 1116 + - Controllers depend on Application types (correct: Infrastructure → Application) 1117 + - Controllers do NOT define types that Application depends on (would be wrong) 1118 + - Infrastructure implements Application contracts, not the other way around 1119 + 1120 + ✅ **Three-Tier Validation Hierarchy**: 1121 + - **Domain**: Invariants enforced in value objects and entities 1122 + - **Application**: Business rules validated in use cases 1123 + - **Infrastructure**: HTTP request structure validated in controllers 1124 + 1125 + ✅ **Separation of Concerns**: 1126 + - Domain logic in entities/value objects 1127 + - Application logic in use cases 1128 + - HTTP transport details in controllers 1129 + - Each layer has clear responsibilities 1130 + 1131 + ### What's Actually Happening (Not a Compromise) 1132 + 1133 + ✅ **Shared Application Layer Types**: 1134 + - `@semble/types` represents the **Application Layer contract** 1135 + - Backend Use Cases produce these contracts 1136 + - Frontend API Client consumes these contracts 1137 + - Both are clients of the Application Layer (this is correct DDD!) 1138 + 1139 + ✅ **Controllers as Thin Adapters**: 1140 + - Controllers adapt HTTP → Application Layer (parse request, call use case) 1141 + - Controllers adapt Application Layer → HTTP (serialize DTO to JSON) 1142 + - No additional mapping needed when DTO shape = HTTP response shape 1143 + - This is a **valid DDD pattern** (Hexagonal Architecture adapter) 1144 + 1145 + ### Why This Is Good DDD (Not Just "OK for Startups") 1146 + 1147 + **From Eric Evans / Martin Fowler:** 1148 + - DTOs exist to cross architectural boundaries ✅ (we do this) 1149 + - Application Layer should be independent of delivery mechanism ✅ (it is) 1150 + - Infrastructure should depend on Application, not vice versa ✅ (correct) 1151 + - Shared kernel is acceptable when bounded contexts align ✅ (monorepo, single app) 1152 + 1153 + **From Hexagonal Architecture (Ports & Adapters):** 1154 + - Application Layer defines "ports" (interfaces/contracts) ✅ (`@semble/types`) 1155 + - Infrastructure provides "adapters" (controllers, API clients) ✅ (thin HTTP adapters) 1156 + - Multiple adapters can implement same port ✅ (backend controller, frontend client) 1157 + - This is the **textbook pattern**! 1158 + 1159 + **When you'd need an additional mapping layer:** 1160 + 1. **API Versioning**: Supporting v1 and v2 simultaneously 1161 + 2. **Different Protocols**: REST + GraphQL + gRPC from same Application Layer 1162 + 3. **Public API**: Need to hide internal structures 1163 + 4. **Legacy Migration**: Gradual transition between representations 1164 + 1165 + For a monorepo with single API version and private API, shared Application types are **architecturally sound**. 1166 + 1167 + ### Dependency Flow (Correct DDD) 1168 + 1169 + ``` 1170 + ┌─────────────────────────────────────────────────────┐ 1171 + │ Domain Layer │ 1172 + │ - Entities (Collection, Card) │ 1173 + │ - Value Objects (CollectionId, URL) │ 1174 + │ - Domain Services │ 1175 + │ No dependencies ↓ │ 1176 + └─────────────────────────────────────────────────────┘ 1177 + 1178 + ┌─────────────────────────────────────────────────────┐ 1179 + │ Application Layer (@semble/types) │ 1180 + │ - Use Cases │ 1181 + │ - DTOs (Collection, GetCollectionsResponse) │ 1182 + │ - Defines contracts for external consumers │ 1183 + │ Depends on: Domain ↑ │ 1184 + └─────────────────────────────────────────────────────┘ 1185 + ↓ ↓ 1186 + ┌──────────────────────┐ ┌──────────────────────┐ 1187 + │ Infrastructure Layer │ │ Presentation Layer │ 1188 + │ - Controllers │ │ - API Client │ 1189 + │ - HTTP Adapters │ │ - Frontend │ 1190 + │ Depends on: App ↑ │ │ Depends on: App ↑ │ 1191 + └──────────────────────┘ └──────────────────────┘ 1192 + ``` 1193 + 1194 + **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. 1195 + 1196 + ### The Real Question: Are Your DTOs Well-Designed? 1197 + 1198 + The quality of this architecture depends on whether your DTOs properly abstract the domain: 1199 + 1200 + ✅ **Good DTO Design** (what you have): 1201 + ```typescript 1202 + // Domain: Rich model with behavior 1203 + class Collection { 1204 + collectionId: CollectionId; // Value object 1205 + name: CollectionName; // Value object with validation 1206 + authorId: UserId; // Value object 1207 + description?: CollectionDescription; // Value object 1208 + cardIds: CardId[]; // List of value objects 1209 + addCard(cardId: CardId) { /* logic */ } 1210 + removeCard(cardId: CardId) { /* logic */ } 1211 + } 1212 + 1213 + // DTO: Simple data structure for transfer 1214 + interface Collection { 1215 + id: string; // Unwrapped value 1216 + name: string; // Unwrapped value 1217 + author: User; // Enriched relationship 1218 + description?: string; // Unwrapped value 1219 + cardCount: number; // Computed property 1220 + createdAt: string; // ISO string 1221 + updatedAt: string; // ISO string 1222 + } 1223 + ``` 1224 + 1225 + This is **good separation**: 1226 + - Domain has rich behavior, validation, invariants 1227 + - DTO is simple, serializable, consumer-friendly 1228 + - Use Case does the mapping (proper layer responsibility) 1229 + 1230 + ❌ **Bad DTO Design** (anti-pattern): 1231 + ```typescript 1232 + // Exposing domain internals 1233 + interface CollectionDTO { 1234 + collectionId: CollectionId; // WRONG: exposing domain value object 1235 + _aggregateVersion: number; // WRONG: exposing internal details 1236 + domainEvents: DomainEvent[]; // WRONG: leaking domain events 1237 + } 1238 + ``` 780 1239 781 - ### Pragmatic Compromises 782 - - ⚠️ Shared types blur application/infrastructure boundary 783 - - ⚠️ Use cases return types designed for HTTP responses 784 - - ✅ But: This is **intentional** and **documented** 1240 + ### Summary: This IS Good DDD 785 1241 786 - ### Why This Is OK for a Startup 787 - 1. **Velocity**: Eliminates significant boilerplate 788 - 2. **Type Safety**: Maintains compile-time guarantees 789 - 3. **Maintainability**: Single source of truth reduces bugs 790 - 4. **Scalability**: Can refactor later if needed (types are internal contract) 1242 + - ✅ Domain → Application → Infrastructure (correct dependency flow) 1243 + - ✅ Use Cases map Domain → DTO (proper anti-corruption layer) 1244 + - ✅ DTOs abstract domain complexity (proper encapsulation) 1245 + - ✅ Multiple adapters consume same Application contract (Ports & Adapters) 1246 + - ✅ Shared Application types in monorepo (Shared Kernel pattern) 791 1247 792 - The key is being **intentional** about the tradeoff rather than accidentally coupling layers. 1248 + **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. 793 1249 794 1250 --- 795 1251