In a previous post, I discussed the impact of variable names, emphasizing the importance of declaring good names, as this directly reflects the quality of the code. This practice is a result of professionalism as a software engineer and directly impacts the readability, maintainability of the code, and the performance of both the individual who writes it and the team as a whole.
However, it is important to highlight that years of experience in the field do not equate to competence. Unfortunately, there are many professionals with years of experience who still work in an amateurish way, with a level of carelessness that compromises the quality of the solutions. In many cases, these solutions are created to solve a single problem but end up addressing multiple other problems in an inefficient manner.
This, however, does not mean that a good professional is immune to making mistakes. On the contrary, an experienced software engineer seeks constant feedback, with a mindset of failing quickly and correcting things swiftly. The point I’m highlighting here is the behavior of inexperienced, amateur, or careless professionals, who unfortunately exist in any field of work.
Vibe coding
“Vibe coding” is a movement that will likely increase the responsibilities of more experienced software engineers. This will happen because, in addition to reviewing code from other developers, it will also be necessary to review code generated by AI tools, which in the future may be produced by teams outside of the technology department, such as commercial, marketing, and other teams.
Currently, these AI tools are effective as support, but they still lack the maturity needed to fully replace a technology professional. They can be extremely useful for speeding up processes or assisting with specific tasks, but they should not be seen as the main tool for software development. Critical review and the ability to deeply understand the context of the problem are skills that, for now, remain exclusive to experienced professionals.
Additionally, there is an important point: skills that are not constantly required tend to deteriorate over time. When not used, these skills lose their sharpness, which can negatively impact the quality of work. In a scenario where tools like AI are on the rise but still rely on human supervision and intervention, it is essential for software engineers to keep their skills sharp and up to date, ensuring that technological evolution is not a threat, but rather an opportunity for growth.
“We are what we repeatedly do. Excellence, then, is not an act, but a habit.” – Aristotle
Choices from the environment
A large part of the guidelines I will share are aimed at a corporate environment, where the focus is not on optimizing machine resources due to technical constraints, as in IoT devices, or on optimizations aimed at specific goals. As I have mentioned in other posts: there are no one-size-fits-all solutions, only trade-offs. Therefore, these guidelines apply to large-scale projects, where there are frequent changes both in the business and in the teams involved.
In this context, some of the key qualities that these practices aim to promote include: readability, modularity, changeability, and maintainability. These quality requirements are essential to ensure that the code remains sustainable and adaptable over time.
Do not use comments
Good code is its own documentation, and I believe this phrase clearly conveys the intention. Becoming proficient at writing clear and efficient code is a fundamental skill for a good software engineer. In fact, learning to code well is just as important as understanding architecture, design patterns, and best practices. Writing in a simple and direct way, so that the code itself becomes its documentation, enhances readability and, consequently, maintainability.
“Comments are a temporary solution to poorly written code. Instead of writing a comment, write better code.” – Martin Fowler
Comments, in addition to increasing the number of lines in a file, create an extra effort to be ignored, and you will inevitably end up ignoring them. They also indicate that the code needs additional explanations to be understood. The only exception would be when documentation needs to be generated from comments, such as in the case of JavaDoc for a library. However, in today’s world of APIs and microservices, this practice is becoming increasingly unnecessary.
It’s worth noting that high-level languages are designed to be closer to human language. This means that the code should be aimed at people, and these languages have evolved to become simpler and more concise, like Swift and Kotlin. However, they do not solve all the ambiguities of human language. For this reason, writing good code is an essential skill for a software engineer, just as a writer has the skill to create a literary work.
Vague, generic, or out-of-context names
int data = 10; // o que é 'data'? pode ser qualquer coisa
Abbreviated or out-of-context names drastically reduce the ability to understand, increasing the cognitive effort to figure out what the variable represents, how it should be populated, whether it’s optional or not, and what the effects of state changes may occur when it is modified.
// o que é 'person data'? pode ser qualquer coisa
PersonData personData = new PersonData();
// o que é 'person info'? pode ser qualquer coisa
PersonInfo personInfo = new PersonInfo();
// qual objeto representa 'o'
// e para o negócio o que info representa como atributo ou comportamento?
o.getInfo();
These objects end up becoming a true “bag” of variables. When the developer does not deeply understand the problem or the business domain, they create generic objects that do not clearly reflect what they represent in the specific context of the application. This happens because, many times, the developer does not delve deeply enough into understanding the problem and handles the variables in a superficial way, without bothering to build an effective model.
This problem arises from a lack of understanding of the business or, often, from a lack of professional maturity. When I talk about understanding the business or business modeling, I’m referring to the knowledge of the expert: the person on the front lines who has studied the domain, understands the processes, masters the specific business language, and knows the technical terms used in that context.
Erik Evans, in his book Domain-Driven Design, highlights the importance of getting close to the business expert and understanding how they think and speak. This expert is the one who truly understands and experiences how the business operates in the real world.
“Ubiquitous Language is a language shared by all team members, including developers, domain experts, and other stakeholders, who work together on the project. It should be used in all communications, both written and spoken, to ensure that everyone has a common understanding of the domain.” – Eric Evans, author of Domain-Driven Design: Tackling Complexity in the Heart of Software (2004)
For ubiquitous language to be effective, it must be reflected in the interactions between the technical team and the business, in the naming of teams, tribes (or any other organizational structure), as well as in the naming of projects, packages, classes, and variables.
“Ubiquitous language connects domain models with code, ensuring that each concept is expressed in the software and that discussions about the software focus on business aspects rather than technical details.”
— Vaughn Vernon, author of Implementing Domain-Driven Design (2013)
Another concern I’d like to raise is that developers are becoming increasingly distanced from business experts—something the signers of the Agile Manifesto strongly emphasized. In modern models, there is often an intermediary role that acts between the business expert and the technical team. However, we’re now facing a shortage of professionals capable of understanding and effectively translating these business rules to the technical team. As a result, the information provided is often imprecise or insufficient, which directly affects the refinement process. This impact carries over into the final solution and, ultimately, the code that is produced.
Names reflect intention.
It is important to note some naming details. For example, when we declare a primitive variable, its name is usually in the singular. However, in the case of collections, such as lists or arrays, we use the plural.
var pessoa = new Pessoa(/*...*/);
var pessoas = new ArrayList<Pessoa>();
Only useful variables.
Unused variables should be removed from the code, as well as unnecessary imports. Extreme Programming (XP) emphasizes continuous refactoring as an essential practice to ensure software quality and simplicity. During the refactoring process, it is crucial that only necessary and tested code remains, eliminating anything that no longer adds value or is no longer in use—avoiding what is commonly referred to as “dead code.”
In addition, XP reinforces the principle of simplicity, which advocates for writing only the code that is strictly necessary to solve the problem at hand. This includes eliminating any piece of code that is unused or does not directly contribute to the value of the software, resulting in solutions that are clearer, more efficient, and easier to maintain.
“Simplicity is the art of removing what isn’t necessary. Unnecessarily added complexity in code only creates problems, not solutions.” — Kent Beck, author of Extreme Programming Explained (1999)
Another principle that reinforces this idea is YAGNI (You Aren’t Gonna Need It). It reminds us to focus only on what is strictly necessary, keeping the code simple and to the point. In other words, we shouldn’t implement pieces of code based on assumptions that we might need them in the future. The goal is to make effective use of development time while reducing the risk of investing effort into something that may never be useful. This leads to code that is clearer, leaner, and free of assumptions—which, more often than not, turn out to be wrong.
It’s important to emphasize that this doesn’t mean we should ignore the need to design with future extensibility in mind. Extensibility should be considered when there is a known, planned problem with a foreseeable implementation. This is very different from adopting a premature overengineering approach. In other words, we should design with abstractions that support extensibility when necessary, but avoid implementing features without a real, predictable need for them.
The point here is: we shouldn’t create implementations without a real, foreseeable need, whether for the present or the near future. Often, doing so is driven by developer ego and ends up introducing unnecessary technical complexity. While it may seem like a “good idea,” this kind of decision can negatively impact the project by making it harder for other developers to understand and adding overhead to maintenance.
Going back to variables, here’s a simple example to illustrate the point: imagine you need to create a mask for a LocalDate
.
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
// ...
public String format(LocalDate date) {
return date.format(formatter);
}
Then you think: “Since I’m already creating a mask for LocalDate
, I might as well prepare one for the LocalDateTime
class too.”
DateTimeFormatter localDateTimeFormatter = DateTimeFormatter.ofPattern("dd/MM/yyyy'T'HH:mm");
DateTimeFormatter localDateFormatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
// ...
public String format(LocalDate date) {
return date.format(localDateFormatter);
}
// ...
You might even think: “I don’t need this for the current task, but since I’m already creating the formatter for LocalDate
, it’s better to go ahead and prepare it for LocalDateTime
as well.”
However, by doing this, you’ve already broken that principle. It’s possible that this formatter will never be used, and you’re polluting the code with unnecessary parts.
Another related principle is KISS (Keep It Simple, Stupid!), which advocates for keeping the code simple and organized. This means creating and using patterns when necessary, but avoiding variables, implementations, or complexities that don’t add real value to the software.
Declare only when necessary.
In the past, compilers were much less sophisticated than those we have today, and a common strategy was to declare variables at the beginning. This allowed the compiler to perform simpler and more efficient optimizations, such as allocating memory in a contiguous and more predictable way. Additionally, it helped the programmer to have a clear view of the variables that would be used in the method. Languages like C and Fortran were examples where this practice was very common, coming from a more traditional and imperative programming model.
However, this approach had its issues. In addition to the program potentially entering an incorrect state by using a variable in the wrong scope, there was also the risk of a memory leak, when the developer forgets to deallocate a variable that is no longer being used.
Nowadays, this practice is no longer recommended. In complex situations, trying to remember the type of the variable, when or how it was initialized can make the code hard to understand. To ensure smoother and more minimalist readability, it is better to declare variables only when they actually need to be initialized, as this increases clarity and reduces the risks of bugs related to improper use of variables.
// evite esse tipo de código
public Transaction process(...) {
LocalDate date;
Transaction transaction;
// ...
date = LocalDate.now();
transaction = new Transaction(..., date);
transaction = transactionService.save(transaction)
return transaction;
}
// essa seria a recomendação
public Transaction process(...) {
// ...
var date = LocalDate.now();
var transaction = new Transaction(..., date);
return transactionService.save(transaction)
}
This reduces the number of lines in a method, lowers the reading effort, makes the code more fluid, prevents initialization failures in longer code, and also eliminates “dirt” in the code, such as unused variables that may be left behind due to logic changes or refactoring.
“Software is a great combination of art and engineering.” — Bill Gates
Another point to avoid is declaring variables in scopes different from where they are actually used. For example, a variable that should be inside a method but is declared outside of it, holding memory longer than necessary. This increases the chances of problems related to memory management and unnecessary resource usage.
int soma = 0; // Declaração da variável temporária fora do método
public void exemplo() {
for (int i = 0; i < 10; i++) {
soma += i;
}
if (soma > 10) {
System.out.println("Soma é maior que 10");
}
}
// essa seria o correto
public void exemplo() {
int soma = 0; // Declaração da variável no menor escopo possível
for (int i = 0; i < 10; i++) {
soma += i;
if (soma > 10) {
System.out.println("Soma é maior que 10");
}
}
}
Establish conventions.
It is essential to establish conventions and standards for architecture and coding in a project. An important standard is variable naming.
In a project I worked on, which was relatively new (and I emphasize that “legacy code” doesn’t necessarily mean bad code), the solution itself addressed a problem in a very interesting way. However, the execution in code was not the most pragmatic. While trying to understand the code, I faced several difficulties, many of them caused by gross mistakes. I will try to illustrate one of the problems encountered:
data class Transaction(
val transactionId: UUID,
... // outras variáveis e
val idTransaction: UUID
)
This class had two IDs. So far, so good, but one represented the identity of the class itself, while the other related to the ID of another system. The effort to figure out which was which became a challenge. Initially, the developers on the team couldn’t explain why we had two variables, and the solution found to understand which one should be used was to search and see where these variables were used or initialized.
It’s not something complicated to investigate once you understand, but it becomes an extra effort. I had to stop someone to explain it to me, investigate the usage of the variables, and only then focus on the solution, which caused a context switch and interrupted my work flow. This makes understanding the system harder, increases the chances of errors, and reduces the productivity of the person coding, as well as affecting the performance of the team as a whole.
In Extreme Programming (XP), among the ten practices, the ninth is the practice of coding patterns. The use of conventions and coding standards is essential to ensure that the code is clear and easily understood by all team members.
Another example of a convention is the variable naming format. In the .NET world, the use of PascalCase is widely conventioned, where all the initials are uppercase. In languages like Java and Kotlin, the convention is camelCase, where the first letter is lowercase and the first letter of subsequent words is uppercase.
Understanding these general conventions is important because, for example, in GoLang, when we declare a public variable, it must start with an uppercase letter, while a private variable must start with a lowercase letter. These conventions are part of the language itself and help maintain consistency in the code.
// Golang
package main
import "fmt"
type Pessoa struct {
Nome string // Pública, pois começa com letra maiúscula
idade int // Privada, pois começa com letra minúscula
}
func (p *Pessoa) SetIdade(i int) {
p.idade = i // Privada, acessada dentro do pacote
}
func (p *Pessoa) GetIdade() int {
return p.idade // Privada, acessada dentro do pacote
}
func main() {
p := Pessoa{"João", 30}
fmt.Println(p.Nome) // Acessando variável pública
// fmt.Println(p.idade) // Erro! Não pode acessar a variável privada diretamente
p.SetIdade(31)
fmt.Println(p.GetIdade()) // Acessando variável privada via método público
}
In JavaScript, there is a popular convention to indicate private variables: they typically start with an underscore (_
). This serves as a way to signal that the variable should not be accessed directly outside the class or module, although, technically, JavaScript does not provide encapsulation as strictly as other languages.
// JavaScript
class Pessoa {
constructor(nome, idade) {
this._nome = nome; // Convenção para variável privada
this._idade = idade; // Convenção para variável privada
}
getNome() {
return this._nome; // Acessando a variável privada via método público
}
setNome(nome) {
this._nome = nome; // Alterando a variável privada via método público
}
getIdade() {
return this._idade;
}
}
const pessoa = new Pessoa("João", 30);
console.log(pessoa.getNome()); // Acessando via método público
pessoa.setNome("Carlos");
console.log(pessoa.getNome()); // Acessando via método público
Be careful with “accents.”
Often, a programmer coming from another language ends up bringing along the “accents” of that language. That’s why it’s important to adapt not only to the conventions of the language but also to the conventions that the team and the company have established.
For example, if a .NET programmer builds a project in Java following the conventions they learned in their “native language,” they might adopt the PascalCase naming model, break lines before opening blocks, and other patterns typical of .NET. When a Java programmer needs to continue or maintain that code, it’s recommended to stick with the convention model already used in the project to avoid creating a mishmash of styles. I know, this can be quite frustrating, but it’s important to keep the project consistent until the end.
If the team decides, by consensus, that the conventions need to change, then the ideal approach is to refactor the entire project to the standard Java convention, which is camelCase. However, this is not always feasible, especially in large projects or with tight deadlines.
Let’s now review some basic conventions to follow:
// nome precisa ser declarativo e sucinto
Pessoa pessoa = new Pessoa(...);
// o fato de estar no plural já indica que pessoas é uma collection
List<Pessoa> pessoas = new ArrayList<>();
// alguns gostam de usar prefixo *is, has* nas variáveis, eu prefiro usar o próprio nome
// muitas vezes já é auto explicativo
boolean ativo = false
// constantes devem ser sempre caixa alta no padrão snake case
public static final Integer VALOR_INICIAL = 10
// evite tipos genéricos em nomes de variáveis
List<String> stringList;
Suppress redundant suffixes and prefixes.
Another common problem is the increased verbosity in variable declarations. Remember that, to code at high performance, we aim to minimize the reading effort. And I will emphasize this several times throughout the text: excessive effort in reading code has negative impacts on productivity. The consequence of this verbosity is that it becomes harder to model and structure a good design, which is one of the main concerns of good software engineers.
Excess is not always a positive thing. An example of this is when we name variables, and in doing so, we end up creating unnecessary redundancies.
“Clean code always seems to have been written by someone who cares.” — Robert C. Martin
// Não precisa repetir o nome da classe ou do tipo na variável
public class Transaction {
private UUID transactionId;
private LocalDate transactionDate;
private BigDecimal valueTransaction;
private LocalDateTime createdDate;
}
Avoid repeating the class or type name in the variable name. The class name is already within a context that defines what it is, so repeating the class name in the attributes doesn’t make sense. For example, in the Transaction class, all the attributes already belong to that class, and repeating the class name in the attributes only increases verbosity without adding value to the object modeling. Repeating the type name in the variables does not make them easier to understand. To avoid this, establish naming conventions for attribute types in the project.
Now, what should we do when attributes of class A contain IDs or references to another class B? In this case, should we use the name of the other class in the attribute name? I’ll leave this reflection for you and develop it in another post.
So, a more minimalist format would be:
public class Transaction {
private UUID id;
private LocalDate date;
private BigDecimal value;
// aqui você convenciona que todo created deve ser um LocalDateTime
private LocalDateTime created;
}
When I say that it’s important to establish conventions, it’s because the obvious often needs to be stated. What’s obvious to some is not always obvious to others. Even though you might believe it should be something natural, it’s often necessary to reinforce these points. Therefore, documenting the conventions or creating an onboarding process that clearly communicates these guidelines, or even offering coaching during pairing in environments that promote best practices, are essential actions. They ensure the continuity of quality maintenance and evolution within the team, as well as promoting a common language among all members, since the team begins to speak and think similarly when writing the code.
Another benefit of these conventions is the direct impact on the use of variables. You notice that with a good convention, the code becomes more fluid, with less text to read, which simplifies understanding, as long as the variables have well-chosen names.
// ...
if (transaction.getValue() > LIMIT_FRAUD_ANALISIS) {
// ...
}
// ...
Avoid long variables.
Remember that our goal is to reduce cognitive effort and increase readability. A very long variable means more words to read and interpret, which increases the difficulty of understanding its purpose. Here’s how an excessively long variable name can be tiring and make it harder to understand its function:
// sem o uso de um padrão convencional
String issoeumtextograndeparamostrarcomopodeserdificilentender = "Certamente"
String mesmoPalavrasComoCamelCaseSeTornaComplicado = "Você percebeu?"
Additionally, it’s important to keep in mind that if you use tools like Checkstyle, Lint, and other similar ones (and I highly recommend their use), they help validate statically whether your code follows the community-recommended standards, which contributes to improving code quality.
For those who have never used these tools, they are also great for learning best practices and developing your coding skills. In this example, if you use an excessively long variable name, you will likely exceed the character limit set in these tools, which will alert you to the violation of best practices. Limiting the number of characters per line aims to improve readability, standardize the code for all developers on the team, force clearer descriptions of variables, and prevent horizontal scrolling in the editor, especially on monitors that are not widescreen.
Avoid generic and ambiguous types.
Providing clear types in the code helps improve the intention and understanding of what is being done. Remember: your code should be its own documentation. The best way to document code is through good modeling, combined with good writing.
Doing good object-oriented or functional modeling is important, but if you declare variables with vague names, composed only of letters and numbers, you will be making the reading, understanding, and consequently, the maintainability of the code harder.
Therefore, avoid using objects, for example, that do not have clear characteristics directly related to the business or the problem you are solving. Here’s an example:
Object pessoa = new Funcionario(...);
List<Object> pessoas = new ArrayList();
Understand that, although the variable name makes it clear what is expected from that object instance, since it is a generic type, it can be overwritten in the following lines with any other type. This means the variable loses its identity and can become anything, which, in large and complex code, increases the chances of errors. Therefore, we should avoid this kind of practice.
How can we keep something sufficiently generic without losing the essence of its purpose? The solution is to use an abstract class that, even though it is generic, limits the types that can be assigned to it. In the case of collections, for example, we can use generics.
This type of approach also impacts typing, weakening one of the main advantages of strong typing, which is compile-time checking. This can lead to runtime errors, as well as break the Liskov Substitution Principle (LSP), which is one of the fundamental principles of object-oriented design.
The LSP states that objects of a derived class should be substitutable for objects of the base class without altering the expected behavior. By failing to ensure this, we risk creating an architecture where the system’s behavior is unpredictable or unsafe.
Pessoa pessoa = new Funcionario(...);
List<Pessoa> pessoas = new ArrayList();
Avoid magic numbers.
Let’s reinforce the mantra: we should write code that is easy to read and understand. Imagine you’re inside a loop for
, and out of nowhere, there’s a calculation where you take a variable’s value and multiply it by a number that just “appears out of thin air” in the middle of the code. Even if that number seems obvious, it disrupts the flow of reading. While going through the code, you suddenly need to interpret what that number means to understand its purpose, which increases cognitive complexity.
To enhance readability, it’s crucial to replace these numbers with variables or constants that have descriptive names. This turns the code into something clearer and easier to read, where everything can be easily interpreted as text and the meaning of each value becomes evident. In doing so, the code becomes more self-explanatory and simpler to maintain.
// perceba que tudo é texto e no meio do texto tem um número que você precisa interpretar
public double area(int raio) {
return 3.14159 * raio * raio;
}
// perceba como ficou mais fluido na leitura um texto e deixa claro o propósito do método
public double area(int raio) {
return PI * raio * raio;
}
Moreover, if the value is reused in other places, consider moving it to a variable within an appropriate class to centralize its usage. Otherwise, if the value is specific to that context, make it a private constant within the appropriate scope.
Conclusion
True, we’ve explored a lot about variables but just scratched the surface of best practices for naming them. There’s a whole world of additional techniques and conventions to ensure clarity and maintainability in code. Diving deeper into topics like context-aware naming, avoiding ambiguous abbreviations, and aligning with style guides could make for an excellent follow-up post! Shall we expand on those ideas next?
The quality of code is not determined solely by the functionality it delivers but by its readability, simplicity, and maintainability over time. A good software engineer knows that well-written code doesn’t need external explanations, as it documents itself through best practices and design choices.
By adopting clear conventions, the developer directly contributes to the creation of a more efficient and collaborative development environment. Code maintenance and evolution become faster when all team members are aligned with a common language and a comprehensible structure.
Principles such as KISS (Keep It Simple, Stupid!), YAGNI (You Aren’t Gonna Need It), and LSP (Liskov Substitution Principle), among others, should be observed to ensure that code is not only functional but also sustainable. The use of tools like linters and checkstyles can help reinforce these practices, promoting cleaner and redundancy-free programming.
In the end, well-written code goes beyond merely implementing functionalities; it reflects professionalism, a deep understanding of the problem, and the ability to clearly communicate complex solutions in a simple and effective way. Thus, by investing in good coding practices, we not only enhance the final product but also improve the team’s environment and overall performance.
In the next article, we will explore some scenarios that should be avoided to ensure that the code remains clean, concise, and easy to maintain.