@schilling.mark.a/software-methodology 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/.github/copilot-instructions.md +106 -0
  2. package/LICENSE +21 -0
  3. package/README.md +174 -0
  4. package/atdd-workflow/SKILL.md +117 -0
  5. package/atdd-workflow/references/green-phase.md +38 -0
  6. package/atdd-workflow/references/red-phase.md +62 -0
  7. package/atdd-workflow/references/refactor-phase.md +75 -0
  8. package/bdd-specification/SKILL.md +88 -0
  9. package/bdd-specification/references/example-mapping.md +105 -0
  10. package/bdd-specification/references/gherkin-patterns.md +214 -0
  11. package/cicd-pipeline/SKILL.md +64 -0
  12. package/cicd-pipeline/references/deployment-rollback.md +176 -0
  13. package/cicd-pipeline/references/environment-promotion.md +159 -0
  14. package/cicd-pipeline/references/pipeline-stages.md +198 -0
  15. package/clean-code/SKILL.md +77 -0
  16. package/clean-code/references/behavioral-patterns.md +329 -0
  17. package/clean-code/references/creational-patterns.md +197 -0
  18. package/clean-code/references/enterprise-patterns.md +334 -0
  19. package/clean-code/references/solid.md +230 -0
  20. package/clean-code/references/structural-patterns.md +238 -0
  21. package/continuous-improvement/SKILL.md +69 -0
  22. package/continuous-improvement/references/measurement.md +133 -0
  23. package/continuous-improvement/references/process-update.md +118 -0
  24. package/continuous-improvement/references/root-cause-analysis.md +144 -0
  25. package/dist/atdd-workflow.skill +0 -0
  26. package/dist/bdd-specification.skill +0 -0
  27. package/dist/cicd-pipeline.skill +0 -0
  28. package/dist/clean-code.skill +0 -0
  29. package/dist/continuous-improvement.skill +0 -0
  30. package/dist/green-implementation.skill +0 -0
  31. package/dist/product-strategy.skill +0 -0
  32. package/dist/story-mapping.skill +0 -0
  33. package/dist/ui-design-system.skill +0 -0
  34. package/dist/ui-design-workflow.skill +0 -0
  35. package/dist/ux-design.skill +0 -0
  36. package/dist/ux-research.skill +0 -0
  37. package/docs/INTEGRATION.md +229 -0
  38. package/docs/QUICKSTART.md +126 -0
  39. package/docs/SHARING.md +828 -0
  40. package/docs/SKILLS.md +296 -0
  41. package/green-implementation/SKILL.md +155 -0
  42. package/green-implementation/references/angular-patterns.md +239 -0
  43. package/green-implementation/references/common-rejections.md +180 -0
  44. package/green-implementation/references/playwright-patterns.md +321 -0
  45. package/green-implementation/references/rxjs-patterns.md +161 -0
  46. package/package.json +57 -0
  47. package/product-strategy/SKILL.md +71 -0
  48. package/product-strategy/references/business-model-canvas.md +199 -0
  49. package/product-strategy/references/canvas-alignment.md +108 -0
  50. package/product-strategy/references/value-proposition-canvas.md +159 -0
  51. package/project-templates/context.md.template +56 -0
  52. package/project-templates/test-strategy.md.template +87 -0
  53. package/story-mapping/SKILL.md +104 -0
  54. package/story-mapping/references/backbone.md +66 -0
  55. package/story-mapping/references/release-planning.md +92 -0
  56. package/story-mapping/references/task-template.md +78 -0
  57. package/story-mapping/references/walking-skeleton.md +63 -0
  58. package/ui-design-system/SKILL.md +48 -0
  59. package/ui-design-system/references/accessibility.md +134 -0
  60. package/ui-design-system/references/components.md +257 -0
  61. package/ui-design-system/references/design-tokens.md +209 -0
  62. package/ui-design-system/references/layout.md +136 -0
  63. package/ui-design-system/references/typography.md +114 -0
  64. package/ui-design-workflow/SKILL.md +90 -0
  65. package/ui-design-workflow/references/acceptance-targets.md +144 -0
  66. package/ui-design-workflow/references/component-selection.md +108 -0
  67. package/ui-design-workflow/references/scenario-to-ui.md +151 -0
  68. package/ui-design-workflow/references/screen-flows.md +116 -0
  69. package/ux-design/SKILL.md +75 -0
  70. package/ux-design/references/information-architecture.md +144 -0
  71. package/ux-design/references/interaction-patterns.md +141 -0
  72. package/ux-design/references/onboarding.md +159 -0
  73. package/ux-design/references/usability-evaluation.md +132 -0
  74. package/ux-research/SKILL.md +75 -0
  75. package/ux-research/references/journey-mapping.md +168 -0
  76. package/ux-research/references/mental-models.md +106 -0
  77. package/ux-research/references/personas.md +102 -0
@@ -0,0 +1,334 @@
1
+ # Enterprise Patterns
2
+
3
+ Patterns for structuring larger systems. These bridge the gap between domain logic and infrastructure concerns.
4
+
5
+ ---
6
+
7
+ ## Repository
8
+
9
+ **When to use:**
10
+ - You want to decouple domain logic from data access
11
+ - You want your business logic to be testable without a real database
12
+ - You want a consistent interface for querying and persisting domain objects
13
+
14
+ **Smell that triggers it:**
15
+ - SQL queries or ORM calls directly inside business logic classes
16
+ - Changing the database requires changes in multiple business classes
17
+ - Unit tests require a running database
18
+
19
+ **Solution:**
20
+ ```
21
+ // Interface defined by the DOMAIN — not by the database
22
+ interface InvoiceRepository:
23
+ findById(id: string): Invoice | null
24
+ findByCustomer(customerId: string): Invoice[]
25
+ findUnpaid(): Invoice[]
26
+ save(invoice: Invoice): void
27
+ delete(id: string): void
28
+
29
+ // Implementation lives in infrastructure layer
30
+ class PostgresInvoiceRepository implements InvoiceRepository:
31
+ constructor(db: DatabaseConnection)
32
+
33
+ findById(id):
34
+ row = this.db.query("SELECT * FROM invoices WHERE id = $1", [id])
35
+ return this.mapToInvoice(row)
36
+
37
+ save(invoice):
38
+ if invoice.isNew():
39
+ this.db.query("INSERT INTO invoices ...", [...])
40
+ else:
41
+ this.db.query("UPDATE invoices SET ... WHERE id = $1", [...])
42
+
43
+ // Business logic knows only the interface
44
+ class InvoiceService:
45
+ constructor(repo: InvoiceRepository)
46
+
47
+ markOverdue():
48
+ unpaid = this.repo.findUnpaid()
49
+ unpaid.forEach(inv => inv.markOverdue())
50
+ unpaid.forEach(inv => this.repo.save(inv))
51
+
52
+ // Test uses an in-memory implementation
53
+ class InMemoryInvoiceRepository implements InvoiceRepository:
54
+ private store: Map<string, Invoice> = new Map()
55
+ findById(id): return this.store.get(id)
56
+ save(invoice): this.store.set(invoice.id, invoice)
57
+ // ... rest of interface
58
+ ```
59
+
60
+ **Key insight:** The interface is owned by the domain layer. The implementation is owned by the infrastructure layer. Business logic never knows which database is behind the repository.
61
+
62
+ ---
63
+
64
+ ## Service Layer
65
+
66
+ **When to use:**
67
+ - You need a clear boundary between the application's business logic and its callers (UI, API, CLI)
68
+ - Multiple entry points (REST API, GraphQL, CLI) need to perform the same business operations
69
+ - You want to define the set of operations an application supports in one place
70
+
71
+ **Smell that triggers it:**
72
+ - Business logic living directly in controllers or route handlers
73
+ - The same business operation duplicated across multiple entry points
74
+ - No clear boundary between "what the application does" and "how it's invoked"
75
+
76
+ **Solution:**
77
+ ```
78
+ // Service layer defines application operations
79
+ class InvoiceService:
80
+ constructor(repo: InvoiceRepository, taxEngine: TaxEngine, notifier: Notifier)
81
+
82
+ createInvoice(data: CreateInvoiceDTO): Invoice
83
+ customer = this.repo.findCustomer(data.customerId)
84
+ invoice = new Invoice(customer, data.lineItems)
85
+ invoice.applyTax(this.taxEngine.calculate(invoice))
86
+ this.repo.save(invoice)
87
+ this.notifier.send(invoice)
88
+ return invoice
89
+
90
+ payInvoice(invoiceId: string, paymentData: PaymentDTO): PaymentResult
91
+ invoice = this.repo.findById(invoiceId)
92
+ invoice.pay(paymentData)
93
+ this.repo.save(invoice)
94
+ return new PaymentResult(invoice)
95
+
96
+ // REST controller — thin, delegates everything
97
+ class InvoiceController:
98
+ constructor(service: InvoiceService)
99
+
100
+ POST /invoices:
101
+ return this.service.createInvoice(requestBody)
102
+
103
+ POST /invoices/:id/pay:
104
+ return this.service.payInvoice(params.id, requestBody)
105
+
106
+ // CLI — same service, different entry point
107
+ class InvoiceCLI:
108
+ constructor(service: InvoiceService)
109
+
110
+ handleCreateCommand(args):
111
+ invoice = this.service.createInvoice(parseArgs(args))
112
+ print(invoice.toString())
113
+ ```
114
+
115
+ **Key insight:** The service layer is the single source of truth for what the application can do. Controllers and CLI handlers are thin wrappers that handle serialization and invocation — nothing else.
116
+
117
+ ---
118
+
119
+ ## Unit of Work
120
+
121
+ **When to use:**
122
+ - Multiple domain objects change as part of a single business operation
123
+ - All changes must succeed or all must fail (transactional consistency)
124
+ - You want to batch database writes rather than writing after every single change
125
+
126
+ **Smell that triggers it:**
127
+ - Multiple `repository.save()` calls in sequence — if the second fails, the first is already committed
128
+ - Business logic that manually manages transactions
129
+
130
+ **Solution:**
131
+ ```
132
+ class UnitOfWork:
133
+ private dirty: DomainObject[] = []
134
+ private newObjects: DomainObject[] = []
135
+ private removedObjects: DomainObject[] = []
136
+
137
+ register(obj: DomainObject):
138
+ this.dirty.push(obj)
139
+
140
+ registerNew(obj: DomainObject):
141
+ this.newObjects.push(obj)
142
+
143
+ registerRemoved(obj: DomainObject):
144
+ this.removedObjects.push(obj)
145
+
146
+ commit(): void
147
+ transaction = db.beginTransaction()
148
+ try:
149
+ this.newObjects.forEach(obj => db.insert(obj))
150
+ this.dirty.forEach(obj => db.update(obj))
151
+ this.removedObjects.forEach(obj => db.delete(obj))
152
+ transaction.commit()
153
+ catch:
154
+ transaction.rollback()
155
+ throw
156
+
157
+ // Service uses UoW for transactional consistency
158
+ class InvoiceService:
159
+ constructor(uow: UnitOfWork, repo: InvoiceRepository)
160
+
161
+ processPayment(invoiceId, paymentData):
162
+ invoice = this.repo.findById(invoiceId)
163
+ payment = new Payment(paymentData)
164
+ invoice.pay(payment)
165
+
166
+ this.uow.register(invoice) ← mark as changed
167
+ this.uow.registerNew(payment) ← mark as new
168
+ this.uow.commit() ← single transaction: both or neither
169
+ ```
170
+
171
+ **Key insight:** Business logic describes *what* changed. Unit of Work handles *when* and *how* it's persisted. If anything fails, nothing is committed.
172
+
173
+ ---
174
+
175
+ ## Specification
176
+
177
+ **When to use:**
178
+ - You have complex business rules that need to be combined, reused, or queried against
179
+ - You need to express "does this object match these criteria?" as a composable object
180
+ - The same selection logic is used in both validation and querying
181
+
182
+ **Smell that triggers it:**
183
+ - Long boolean expressions for filtering or validation
184
+ - The same filtering logic duplicated in business rules and database queries
185
+ - Complex conditions that are hard to name or reuse
186
+
187
+ **Solution:**
188
+ ```
189
+ interface Specification:
190
+ isSatisfiedBy(candidate): boolean
191
+ and(other: Specification): Specification
192
+ or(other: Specification): Specification
193
+ not(): Specification
194
+
195
+ class UnpaidInvoiceSpec implements Specification:
196
+ isSatisfiedBy(invoice): return invoice.status == "unpaid"
197
+
198
+ class OverdueDateSpec implements Specification:
199
+ constructor(referenceDate: Date)
200
+ isSatisfiedBy(invoice): return invoice.dueDate < this.referenceDate
201
+
202
+ class MinimumAmountSpec implements Specification:
203
+ constructor(minAmount: Money)
204
+ isSatisfiedBy(invoice): return invoice.total() >= this.minAmount
205
+
206
+ // Compose specifications
207
+ overdueAndUnpaid = new UnpaidInvoiceSpec().and(new OverdueDateSpec(today))
208
+ highValueOverdue = overdueAndUnpaid.and(new MinimumAmountSpec(Money(10000)))
209
+
210
+ // Use in business logic
211
+ invoices.filter(inv => highValueOverdue.isSatisfiedBy(inv))
212
+
213
+ // Use in validation
214
+ if not unpaidSpec.isSatisfiedBy(invoice):
215
+ throw Error("Invoice is not in unpaid state")
216
+ ```
217
+
218
+ **Key insight:** Business rules become named, composable objects. "High-value overdue unpaid invoices" is expressed as composed specifications, not a deeply nested boolean expression.
219
+
220
+ ---
221
+
222
+ ## Value Object
223
+
224
+ **When to use:**
225
+ - An object represents a concept defined entirely by its attributes, not by identity
226
+ - Two instances with the same attributes are considered equal
227
+ - The object should be immutable once created
228
+
229
+ **Smell that triggers it:**
230
+ - Raw primitives (numbers, strings) representing domain concepts like money, email, coordinates
231
+ - Repeated validation logic for the same concept scattered across the codebase
232
+ - Equality checks on raw values that should carry meaning
233
+
234
+ **Solution:**
235
+ ```
236
+ class Money:
237
+ constructor(amount: number, currency: string)
238
+ if amount < 0: throw Error("Amount cannot be negative")
239
+ if not validCurrency(currency): throw Error("Invalid currency")
240
+ this.amount = amount
241
+ this.currency = currency
242
+ freeze(this) ← immutable
243
+
244
+ add(other: Money): Money
245
+ if this.currency != other.currency:
246
+ throw Error("Cannot add different currencies")
247
+ return new Money(this.amount + other.amount, this.currency)
248
+
249
+ multiply(factor: number): Money
250
+ return new Money(this.amount * factor, this.currency)
251
+
252
+ equals(other: Money): boolean
253
+ return this.amount == other.amount && this.currency == other.currency
254
+
255
+ toString(): string
256
+ return formatCurrency(this.amount, this.currency) // "$1,000.00"
257
+
258
+ class Email:
259
+ constructor(address: string)
260
+ if not isValidEmail(address): throw Error("Invalid email: " + address)
261
+ this.address = address.toLowerCase()
262
+ freeze(this)
263
+
264
+ equals(other: Email): boolean
265
+ return this.address == other.address
266
+
267
+ // Usage — no raw primitives in domain logic
268
+ class Invoice:
269
+ constructor(customer: Customer, total: Money)
270
+ this.customer = customer
271
+ this.total = total // Money, not number
272
+
273
+ addLineItem(item: LineItem):
274
+ this.total = this.total.add(item.amount) // Money.add, not +
275
+ ```
276
+
277
+ **Key insight:** Value objects move validation to the point of creation. A `Money` object can never hold an invalid amount. A raw number can. Every domain concept that has validation rules and natural equality should be a value object.
278
+
279
+ ---
280
+
281
+ ## Domain Events
282
+
283
+ **When to use:**
284
+ - Something happened in the domain that other parts of the system should know about
285
+ - You want to decouple the thing that happened from the reactions to it
286
+ - You want an audit trail of what occurred in the domain
287
+
288
+ **Smell that triggers it:**
289
+ - A method that directly calls multiple other systems after completing its work
290
+ - Business logic that knows about email, logging, or external APIs
291
+ - No record of what happened in the domain over time
292
+
293
+ **Solution:**
294
+ ```
295
+ // Events are simple data objects — immutable records of what happened
296
+ class InvoiceCreatedEvent:
297
+ constructor(invoiceId, customerId, amount, createdAt)
298
+ // No behavior — just data
299
+
300
+ class InvoicePaidEvent:
301
+ constructor(invoiceId, transactionId, paidAt)
302
+
303
+ // Domain object collects events, doesn't publish them
304
+ class Invoice:
305
+ private events: DomainEvent[] = []
306
+
307
+ markPaid(transactionId):
308
+ this.status = "paid"
309
+ this.events.push(new InvoicePaidEvent(this.id, transactionId, now()))
310
+
311
+ getEvents(): DomainEvent[]
312
+ return this.events
313
+
314
+ // Service publishes events AFTER the transaction commits
315
+ class InvoiceService:
316
+ constructor(repo: InvoiceRepository, eventBus: EventBus)
317
+
318
+ payInvoice(invoiceId, paymentData):
319
+ invoice = this.repo.findById(invoiceId)
320
+ invoice.markPaid(paymentData.transactionId)
321
+ this.repo.save(invoice)
322
+ invoice.getEvents().forEach(e => this.eventBus.publish(e))
323
+
324
+ // Handlers react to events — completely decoupled
325
+ class EmailOnPaymentHandler:
326
+ handle(event: InvoicePaidEvent):
327
+ sendPaymentConfirmation(event.invoiceId)
328
+
329
+ class AuditOnPaymentHandler:
330
+ handle(event: InvoicePaidEvent):
331
+ auditLog.record(event)
332
+ ```
333
+
334
+ **Key insight:** The domain object doesn't know who's listening. It just records what happened. The event bus delivers events to handlers after the fact. New reaction = new handler, zero changes to the domain.
@@ -0,0 +1,230 @@
1
+ # SOLID Principles
2
+
3
+ ## Single Responsibility Principle (SRP)
4
+
5
+ **Rule:** A class has exactly one reason to change.
6
+
7
+ **Smell that triggers it:**
8
+ - Class has methods that serve different concerns
9
+ - Changing one feature requires editing this class even though the feature has nothing to do with the class's core purpose
10
+ - Class name is vague or uses "and" (e.g., `InvoiceManager`)
11
+
12
+ **Before:**
13
+ ```
14
+ class Invoice:
15
+ calculateTotal() ← business logic
16
+ formatAsPDF() ← presentation
17
+ sendByEmail() ← communication
18
+ saveToDatabase() ← persistence
19
+ ```
20
+
21
+ **After:**
22
+ ```
23
+ class Invoice: ← owns business logic only
24
+ calculateTotal()
25
+
26
+ class InvoicePDFRenderer: ← owns presentation
27
+ render(invoice)
28
+
29
+ class InvoiceNotifier: ← owns communication
30
+ send(invoice)
31
+
32
+ class InvoiceRepository: ← owns persistence
33
+ save(invoice)
34
+ ```
35
+
36
+ **Key insight:** "Reason to change" means a business requirement change. If marketing changes the PDF layout, only `InvoicePDFRenderer` changes. If tax rules change, only `Invoice` changes.
37
+
38
+ ---
39
+
40
+ ## Open/Closed Principle (OCP)
41
+
42
+ **Rule:** Open for extension, closed for modification.
43
+
44
+ **Smell that triggers it:**
45
+ - Adding a new variant requires editing existing code
46
+ - if/else or switch statements that grow with each new requirement
47
+ - A method that checks "what type of thing am I dealing with?"
48
+
49
+ **Before:**
50
+ ```
51
+ class TaxCalculator:
52
+ calculate(invoice):
53
+ if invoice.region == "US":
54
+ return invoice.total * 0.08
55
+ if invoice.region == "UK":
56
+ return invoice.total * 0.20
57
+ if invoice.region == "CA": ← every new region edits this class
58
+ return invoice.total * 0.05
59
+ ```
60
+
61
+ **After:**
62
+ ```
63
+ interface TaxRule:
64
+ calculate(invoice): Money
65
+
66
+ class UStaxRule implements TaxRule:
67
+ calculate(invoice): return invoice.total * 0.08
68
+
69
+ class UKTaxRule implements TaxRule:
70
+ calculate(invoice): return invoice.total * 0.20
71
+
72
+ class TaxCalculator:
73
+ constructor(rule: TaxRule) ← new region = new class, no edits
74
+ calculate(invoice): return this.rule.calculate(invoice)
75
+ ```
76
+
77
+ **Key insight:** New behavior is added by writing new code (new class), not by editing existing code. The `TaxCalculator` never changes again.
78
+
79
+ ---
80
+
81
+ ## Liskov Substitution Principle (LSP)
82
+
83
+ **Rule:** A subtype must be usable anywhere its base type is expected, without changing behavior.
84
+
85
+ **Smell that triggers it:**
86
+ - A subclass overrides a method and throws an exception or returns nothing
87
+ - Code that checks `if (thing instanceof SpecificSubclass)` before acting
88
+ - A subclass that weakens a precondition or strengthens a postcondition
89
+
90
+ **Before:**
91
+ ```
92
+ class Shape:
93
+ area(): number
94
+
95
+ class Circle extends Shape:
96
+ area(): return π * r²
97
+
98
+ class Square extends Shape:
99
+ area(): return side²
100
+
101
+ class ReadOnlySquare extends Square:
102
+ setSize(): throw Error("Cannot modify") ← violates LSP
103
+ ```
104
+
105
+ **After:**
106
+ ```
107
+ interface Shape:
108
+ area(): number ← read-only contract
109
+
110
+ class Circle implements Shape:
111
+ area(): return π * r²
112
+
113
+ class Square implements Shape:
114
+ area(): return side²
115
+
116
+ class MutableSquare extends Square:
117
+ setSize(s): this.side = s ← mutation is opt-in, not broken inheritance
118
+ ```
119
+
120
+ **Key insight:** If a subclass has to break the parent's contract to work correctly, the hierarchy is wrong. Flatten it or use composition.
121
+
122
+ ---
123
+
124
+ ## Interface Segregation Principle (ISP)
125
+
126
+ **Rule:** Clients should not be forced to depend on methods they do not use.
127
+
128
+ **Smell that triggers it:**
129
+ - A class implements an interface but leaves methods empty or throwing
130
+ - An interface with many methods, most of which any given consumer ignores
131
+ - "Fat interface" that accumulates methods over time
132
+
133
+ **Before:**
134
+ ```
135
+ interface Repository:
136
+ findById(id)
137
+ findAll()
138
+ save(entity)
139
+ delete(entity)
140
+ bulkImport(entities) ← only used by one import service
141
+ generateReport() ← only used by reporting
142
+ ```
143
+
144
+ **After:**
145
+ ```
146
+ interface Reader:
147
+ findById(id)
148
+ findAll()
149
+
150
+ interface Writer:
151
+ save(entity)
152
+ delete(entity)
153
+
154
+ interface BulkImporter:
155
+ bulkImport(entities)
156
+
157
+ class InvoiceRepository implements Reader, Writer, BulkImporter:
158
+ // ... implements all
159
+
160
+ class InvoiceService:
161
+ constructor(reader: Reader, writer: Writer) ← only sees what it needs
162
+ ```
163
+
164
+ **Key insight:** Each consumer declares the interface it needs. The concrete class implements all of them. Consumers are decoupled from each other.
165
+
166
+ ---
167
+
168
+ ## Dependency Inversion Principle (DIP)
169
+
170
+ **Rule:** High-level modules depend on abstractions. Low-level modules also depend on abstractions. Abstractions do not depend on details.
171
+
172
+ **Smell that triggers it:**
173
+ - A business logic class directly instantiates or imports infrastructure (database, HTTP client, file system)
174
+ - Changing a data store requires editing business logic
175
+ - Unit tests require a real database or network call
176
+
177
+ **Before:**
178
+ ```
179
+ class InvoiceService:
180
+ constructor():
181
+ this.db = new PostgresDatabase() ← concrete dependency
182
+ this.emailer = new SMTPEmailer() ← concrete dependency
183
+
184
+ createInvoice(data):
185
+ invoice = new Invoice(data)
186
+ this.db.save(invoice) ← can't test without Postgres
187
+ this.emailer.send(invoice) ← can't test without SMTP
188
+ ```
189
+
190
+ **After:**
191
+ ```
192
+ interface InvoiceRepository: ← abstraction
193
+ save(invoice)
194
+
195
+ interface Notifier: ← abstraction
196
+ send(invoice)
197
+
198
+ class InvoiceService:
199
+ constructor(repo: InvoiceRepository, notifier: Notifier) ← depends on abstractions
200
+ this.repo = repo
201
+ this.notifier = notifier
202
+
203
+ createInvoice(data):
204
+ invoice = new Invoice(data)
205
+ this.repo.save(invoice)
206
+ this.notifier.send(invoice)
207
+
208
+ // Production wiring:
209
+ service = new InvoiceService(new PostgresInvoiceRepository(), new SMTPNotifier())
210
+
211
+ // Test wiring:
212
+ service = new InvoiceService(mockRepository, mockNotifier) ← no real infra needed
213
+ ```
214
+
215
+ **Key insight:** The business logic (`InvoiceService`) never knows about Postgres or SMTP. It knows only the contracts it needs. This is the foundation that makes unit testing fast and isolated.
216
+
217
+ ---
218
+
219
+ ## Applying SOLID in Practice
220
+
221
+ **Priority order when multiple principles apply:**
222
+ 1. DIP first — if you can't inject dependencies, nothing else matters for testability
223
+ 2. SRP — identify the single responsibility before extracting
224
+ 3. OCP — once responsibilities are clear, make extension points
225
+ 4. ISP — split interfaces when consumers only need subsets
226
+ 5. LSP — verify inheritance hierarchies don't break contracts
227
+
228
+ **During GREEN:** Only apply DIP if the current test literally cannot pass without it (e.g., test needs to inject a mock). Everything else waits for REFACTOR.
229
+
230
+ **During REFACTOR:** Walk through the list above. Apply only what the current code requires. Rule of Three applies — don't extract until the second occurrence.