I tend to observe that junior engineers simply misplace the level of complexity that a system needs in order to be as simple as possible. Meaning, I've seen errors to the side of being too simple, and errors to the side of being too complex; the unifying theory here is that its a mis-estimation of "necessary complexity".
Two examples:
In designing an email templating system, I've seen junior engineers devise really complex class hierarchies, with super classes that take in type generics for their super() constructor, three levels deep, resulting in hundreds of lines of code. When pressed, the reasoning was "there are parts of the email body we want to be the same and parts that are different, so the class hierarchies map to those parts that are similar vs different." It turns out, this service was only sending 3 different email "types" for the foreseeable future, and the only similarity between them was a copyright notice at the bottom, a banner at the top, and a body.
But, a counterpoint: For a long while, on some node.js backend API we were writing, much of the core business logic was implemented as simple functions across hundreds of files. Worked great when we had four API routes. Now, with hundreds, its impossible to find anything, because there's no structure, patterns, or organization. So, we needed someone to step in and introduce rules and structure in order to make the system more simple, and our job would have been easier if we'd had the foresight to see that necessary complexity coming.
> In designing an email templating system, I've seen junior engineers devise really complex class hierarchies, with super classes that take in type generics for their super() constructor, three levels deep, resulting in hundreds of lines of code. When pressed, the reasoning was "there are parts of the email body we want to be the same and parts that are different, so the class hierarchies map to those parts that are similar vs different." It turns out, this service was only sending 3 different email "types" for the foreseeable future, and the only similarity between them was a copyright notice at the bottom, a banner at the top, and a body.
Was there a discussion about high-level design of the templating system at all before the junior started banging out code? This feels more like a failure of the senior/lead.
> But, a counterpoint: For a long while, on some node.js backend API we were writing, much of the core business logic was implemented as simple functions across hundreds of files. Worked great when we had four API routes. Now, with hundreds, its impossible to find anything, because there's no structure, patterns, or organization. So, we needed someone to step in and introduce rules and structure in order to make the system more simple, and our job would have been easier if we'd had the foresight to see that necessary complexity coming.
This feels like a false dichotomy to me. Your system existed in a state somewhere between between 4 routes and hundreds, didn't it? At the point something starts being painful is when it should be addressed, imo. That was probably somewhere in the teens of routes would be my guess. At that point the system is still small and understandable enough to make a large architectural change, but it's big enough that you can be reasonably certain you're making the correct large architectural change.
> Was there a discussion about high-level design of the templating system at all before the junior started banging out code? This feels more like a failure of the senior/lead.
There was not. This is a startup I'm referencing; we don't hold long technical planning sessions for how to design a few hundred lines of code. We'd likely hold a meeting on the product behind it, but that's not what we're talking about here.
Does that mean it was a failure of leadership? I'd argue No. I would argue that it might be a failure in mentorship, which is different, but in all the startups I've been in, there's one constant: Engineers have MASSIVE freedom and responsibility to get the product out the door. We don't micromanage the implementations. That doesn't mean they always turn out great, and that's where mentorship comes in; teaching how to fish instead of the seniors doing it for them.
> This feels like a false dichotomy to me. Your system existed in a state somewhere between between 4 routes and hundreds, didn't it? At the point something starts being painful is when it should be addressed, imo. That was probably somewhere in the teens of routes would be my guess. At that point the system is still small and understandable enough to make a large architectural change, but it's big enough that you can be reasonably certain you're making the correct large architectural change.
I think you're right. No one is perfect.
The reality of many startups is that the senior developers are vastly overworked. I mean, everyone is overworked, but at least in my experience, it comes back to the fact that the seniors can't micromanage everyone. So at some point, we were at a dozen routes, and it probably sucked to work in, but its likely that the seniors reviewed it and "missed the forest for the trees". Yeah, that code looks great, ship it, next feature, keep growing.
It takes a different, holistic perspective to see that the whole architecture is slowly getting bad, and often you don't experience that different perspective working in the weeds on each route. Combine that with the fact that startups tend to move REALLY fast, and an engineering department four the size still wouldn't have the capacity to take on all the feature work we want, let alone the refactoring, and its easy to understand why architecture gets pushed off.
Surely good examples, but the later may have been the right path: start simple with what is necessary. When there is a need for a framework or some layer of complexity - add it then. But maybe not before.
And #1 is funny because we've all done something like that!
Two examples:
In designing an email templating system, I've seen junior engineers devise really complex class hierarchies, with super classes that take in type generics for their super() constructor, three levels deep, resulting in hundreds of lines of code. When pressed, the reasoning was "there are parts of the email body we want to be the same and parts that are different, so the class hierarchies map to those parts that are similar vs different." It turns out, this service was only sending 3 different email "types" for the foreseeable future, and the only similarity between them was a copyright notice at the bottom, a banner at the top, and a body.
But, a counterpoint: For a long while, on some node.js backend API we were writing, much of the core business logic was implemented as simple functions across hundreds of files. Worked great when we had four API routes. Now, with hundreds, its impossible to find anything, because there's no structure, patterns, or organization. So, we needed someone to step in and introduce rules and structure in order to make the system more simple, and our job would have been easier if we'd had the foresight to see that necessary complexity coming.