Are your engineering “best practices” just developer dogmas?
My biggest dilemma when I started to program was this: How do I write things correctly?
Should a controller directly access the repository or use a service method as an intermediary? Where should we validate objects – within the Controller or the Service layer? Should repositories be placed in a repository package or a feature package?
There are endless ways to write code, but there must be a right way. To settle the ongoing debates, I turned to established best practices. Best practices provide limitations to the endless landscape of programming options. It is a lot easier to answer the afore-posed questions when an authority suggests that:
- Controllers should typically avoid direct access to repositories, with a few exceptions.
- Object validation is often best placed within a Controller.
- Feature packages tend to be more practical than technical packages.
Embracing these best practices both streamlined my decision-making process and helped resolve disagreements with fellow developers. Why engage in heated debates about the superiority of feature packages when you can simply reference authoritative articles? When those references come from a reputable source like Wikipedia, the argument is immediately over, right?
The problem arises when people take these practices for granted, and they turn into dogmas that never get questioned.
The problem with dogmas
Best practices in software development are not static and can evolve.
Consider the evolution of database access patterns. When starting my development career, the best practice for accessing a repository (at least in Java) was through DAO objects, which is now considered outdated in favor of repositories. If we look at other data source architectural patterns, we could add many more of these patterns to the equation. How do we know which one of these best practices is the best? The key lies in developing a deep understanding of these practices.
Without comprehending why a particular practice is considered good, you risk making bad decisions. Such decisions can lead you to “hype-driven development,” where you adopt the latest trends without fully understanding their implications. This can result in complex, service-oriented, NoSQL, Big Data monsters that nobody knows how to maintain anymore.
Questioning everything, even the most widely accepted authorities, is a valuable approach. Instead of blindly following trends, consider the context and the trade-offs of each practice.
You need an Igor in your life
I had a colleague (let’s call him Igor), a skeptic by nature.
He was unimpressed by dogmas, both in his professional and private life. He was an old-school developer who clung to his tried-and-true methods, largely unfazed by new ideas and trends in our field.
Convincing Igor to accept a new coding practice was not an easy thing to do. It would even get terribly frustrating if you didn’t fully understand the practice yourself. Suppose Igor is dissatisfied with grouping packages by feature rather than by technology. Here is how a conversation with Igor might unfold when we don’t understand why we do things the way we do them:
- I don’t know why repositories are suddenly placed in packages named by entities.
- Well, putting them in the repository package is not a good practice. It doesn’t foster Domain Driven Design.
- What is this Domain Driven Design?
- Well, it states that you should organize your code by domains, not technology. That domain thinking should drive how you design things.
- And what’s wrong with the way we’ve been coding so far?
- Well, the domain knowledge is not represented in the code.
- You want to say that you understand the domain behind this software we are writing.
- Not right now, but…
- The analytics department just hands us use cases, and we code what we are given. Nobody asks us anything.
- So you think you know better than all these people that have written this and have been adopting this?!
- I don’t care what some fool has written. Today, they promote this as the best thing, tomorrow, it will be something else.
There is no way to win the argument this way, really. It immediately steers off to terminologies rather than answering the question and uses authorities instead of providing explanations. There is no real understanding behind these terminologies.
What if we approached the same argument differently?
- I don’t know why repositories are suddenly placed in packages named by entities.
- Why would you rather place them in a repository package?
- Because you do not need to think much about where to put a class, it’s straightforward.
- Let me ask you a question: do you spend more time reading or writing code?
- Definitely reading, trying to understand what the heck is going on in there.
- How many classes do you need to understand a certain use case?
- Controller, service, repository, a couple of classes.
- When using your IDE, would you rather have these classes close to each other or scattered across the project?
- Close, I guess.
- This is what ‘Package by Feature’ is all about. The idea is that packages should contain related concepts that you would usually look at simultaneously. I’m pretty sure that when reading AccountService, the next class you are going to look at is AccountRepository and not CountryService, right?
- Yeah, but it’s harder to think now which class goes where
- Yeah, I agree it is harder, but remember you will read your code more than write it.
My encounters with Igor made me question, dissect, and refine my understanding of best practices. Each new concept had to withstand scrutiny and provide rock-solid answers to a stream of whys. Was it genuinely a best practice? Were there situations where it didn’t apply? This rigorous approach ultimately led me to a point where the questions became less necessary, and the knowledge became more ingrained.
There is only one real dogma
Money.
It serves as the ultimate driver behind our decisions and actions. To understand and deconstruct any dogma, you can employ a series of questions until we arrive at an answer that inevitably involves creating more value (money).
Why should we write clean code – and what happens if we don’t?
- Bad code is harder to understand + harder to maintain = requires more money to change.
- Applications that are hard to change eventually get dropped in favor of new ones (more money).
Why should we write tests? What happens if we don’t?
- We need to employ testers (which costs money) who need not only to test the new feature but also to test against regression.
- Regression can be introduced that not even the tester can catch (which loses us money).
- The feedback cycle between devs and the quality of the solution is increased. The developer needs to wait for the testers’ approval (or disapproval) before merging the code. The feature is released later than sooner (losing more money).
- Non-functional code changes are discouraged since they require extensive regression testing (and, therefore, cost more in man-hours). This means the software will soon become ugly, inheriting the same drawbacks as not writing clean code from above.
Engaging in these mental exercises can greatly assist in organizing and solidifying your knowledge. Once mastered, these conclusions can be used as corollaries for more complex questions.
Why is the microservices architectural style popular?
Why are applications favoring embedded Tomcat over traditional Tomcat?
Why should product managers drive epics instead of developers?
Context matters
In addressing complex problems, a thorough exploration often requires delving into the historical context of best practices and considering various aspects that influenced their inception.
- What specific problems did engineers face at that time?
- Were resources readily available or constrained?
- What tools were available at that time?
- How were organizations structured?
- Did developers take on testing and deployment?
- Did they write SQL code?
- Did they possess a deep understanding of the domain?
Take, for instance, the common practice of prefixing interface class names with the letter ‘I.’ Some consider this a best practice, but why?
It comes from a time when IDE-s were not that sophisticated, where developers needed to put prefixes in names to differentiate a regular class from an interface. There were also no ‘this’ pointers, so class members were prefixed with ‘m’, while method parameters were prefixed with ‘p’.
Good books will usually cover these topics, but it’s not something that an engineer will remember during their first reading. That’s why these practices need to be revisited until they click. You know it clicked if you could explain them to Igor.
There is no one right way
Do note that both you and Igor might be right at the same time!
In the conversation above, Igor stated that it’s harder to know where each class goes when packaging classes by feature. It requires people to define the boundaries of a feature.
What if the feature undergoes a significant transformation, as it often happens? Adapting the packages becomes necessary, implying an evolutionary architecture. Do your team members possess the skills for such adaptability? You might not even be on the same project to enforce these practices in a month.
Beyond understanding why something is good, researching why something is bad can be equally enlightening. A simple Google search for ‘why microservices suck’ reveals how even established authorities don’t consider microservices a silver bullet. For example, StackOverflow, a successful platform, operates as a monolithic application on on-premise servers, defying the cloud-microservices paradigm. The point is not that microservices are inherently flawed but that deeper comprehension will always outperform blind adherence to trends.
Building a deeper understanding can take various forms, but the core message I leave you with is simple. Never take things for granted and always ask: