>I believe OOP and relational database get a lot of flack because programmers tend to be bad at design, not because they are broken paradigms.

OOP has Fundamental and Intrinsic problems that can be described in a very concrete way.

If you believe OOP gets a lot of flack just because programmers are bad at design, then you are the one that is also bad at design.

I will say this, OOP is bad for many and most design problems, and most people who hate OOP also don't even know why OOP is bad. They just have this gut feeling and bad experiences but they can never pinpoint the concrete reason for why OOP tends to lead to bad designs. A lot of people who like OOP, really like the mind bending design patterns but they don't realize that these patterns often offer limited flexibility and the catharsis of creating a design pattern abstraction is just an illusion.

So really what's going on is nobody knows the true nature and goal of design in programming. There's no theory behind it. To first know why OOP is bad you need to know the fundamental nature and goal of design when it comes to programs.

The goal and nature of design is abstraction. When designing programs we want to start from primitives, then compose those primitives into higher level abstractions, then take those abstractions and also form those into even higher level abstraction until we achieve the final level of abstraction that represents the program itself. The key insight here is because everything starts with a primitive, the way your abstractions are designed, depends entirely on your primitives and choice of primitives.

A good primitive must be able to additively compose with other primitives to form every other possible abstraction that the program may possibly need. "Additively" being the keyword here because if you subtract information from your primitive during composition it means your primitive is not "primitive" enough and that in actuality the "thing" your dealing with may be two primitives representing what you tried to "subtract" from the primitive and the remaining part of the original primitive itself.

Bad design often involves dealing with bad primitives. You may find yourself realizing that you have abstractions that cannot be built out of the composition of the primitives that you have. You may find that you have to split up one of your primitives and realize that since it's all encapsulated in a micro-service there's no easy way to do this, so you take on technical debt by making a redundant component that does what you need. You may not even have a notion of what the primitives of your programs are and designed things at the highest level of abstraction with Zero code re-use. 99.999% of all programmers will not have a notion of what primitives are, and design programs in a way where they have a potpourri of components that are a mishmash of high level logic combined with low level logic and no notion of forming higher levels of logic with lower level composition of primitives. Yes 99% of programmers are like this, literally take a look at yourself and your colleagues and tell me who out of all them actually has linked the notion of the "design of programs" with "choice of primitives." In fact the entire blog post never mentioned the word "primitive" or "axiom" once in the entire write up on design.

Which brings me back to the initial topic of why OOP is bad:

OOP is bad because the object is a bad primitive.

Objects are actually arbitrary mixtures of lower level primitives: functions and data. It is far easier to compose functions with functions and data with data than it is to compose these arbitrary mixtures called objects with other objects. Object compositions often involve surgical grafts of one object into another object resulting in a hideous dependency. A lot of people like to use tricks and have this happen at runtime; People tend to call it dependency injection, a very abstract and clever concept but also very very very bad.

Meanwhile composing two arrays:

[1,2,3] + [4,5,6] = [1,2,3,4,5,6]

Composing two functions:

function compose(f, g) {
return function(x){
return f(g(x))
}
}

b = function(x){return x+1}
c = function(y){return y*2}
a = function(j){return j-3}
d = function(e){return e*e}

g = compose(b,c) // g(x) = (x*2)+1
t = compose(a,b) // t(x) = (x+1)-3
l = compose(d,d) // l(x) = x*x*x

... you get the picture.