I think a key distinction often lost here is that generic code and abstract code are different. Abstract code hides details, generic code allows its use in more places. When hiding details, often it also becomes more generic. Making code generic does not necessarily hide details, it can very well often expose additional details

Also seemingly not mentioned - SRP (single responsibility principle). SRP & DRY should be considered together. If a person DRY'ies up code without regard to SRP, they're making any code that can be generic, generic. A rule of thumb is generic code is 3x more expensive than non-generic code.

==============

To illustrate, here is an example (and pretend that these examples are duplicated in 20 different places that all need the account balance sum):

--------------

Example (1) - non-generic, non-abstact

```

int savingsBalance = 1;

int checkingBalance = 1;

int totalBalance = savingsBalance + checkingBalance;

```

--------------

Example (2) - generic, minimally abstract

```

int savingsBalance = 1;

int checkingBalance = 1;

int totalBalance = addBalances(savingsBalance, checkingBalance);

```

--------------

Example (3) - abstract, potentially generic:

```

int totalBalance = addBalances();

```

===============

Now consider what happens if we need to add a 'brokerage account balance' to the mix (and let's say we get that value via an API call). These example change in the following ways:

Example (1), updated:

```

int savingsBalance = 1;

int checkingBalance = 1;

int brokerageBalance = fetchBrokerageBalance();

int totalBalance = addBalances(savingsBalance, checkingBalance, brokerageBalance);

```

Example (2), updated:

```

int savingsBalance = 1;

int checkingBalance = 1;

int brokerageBalance = fetchBrokerageBalance();

int totalBalance = addBalances(savingsBalance, checkingBalance, brokerageBalance);

```

Example (3), updated & unchanged:

```

int totalBalance = addBalances();

```

Example (1) & Example (2) have similar scaling behavior here (scaling relative to complexity). This illustrates a very key difference between abstract and generic code.

Now, let's say on another hand that whether we should include brokerage balance is conditional. In example 1, we have the same logic to be applied in 20 different places. We can mutate example 3 to be more generic (EG: pass in flag - `addBalances(Flags.includeBrokerageAccount)`). At this point we can say that the abstraction is wrong and needs to be split into different methods (which is fine!). Making example 3 more generic is more complex, we incur the penalty of having generic code. Example 1 is arguably the worst to have since we will get subtle errors if we fail to update everything. In part these design principles are also there to help protect updates and make them safe (very similar to the ACID guarantees of database that help make it so you can update data without breaking the overall database)

Another mention, which I won't go into detail over, boiler plate code has yet different characteristics.

In sum, it's largely a question of what kind of coupling is best and how to deal with that coupling. Duplicated code is coupled without any runtime or compile time checks that it stays in sync (if you forget to update something from example 1 above, it's a bug!). Keeping code consolidated into a common procedure does not remove that coupling, it just changes the nature of the coupling and makes it more explicit. Common code between micro-services couples those micro-services together (and that can be very bad).

Thus, we need to look at a lot of things when applying DRY: we need to consider SRP, whether we are coupling services together, and whether or not if we are simplying making non-generic code generic.