In a previous post, I discussed the impact of variable names and the importance of choosing good ones, since that choice directly reflects on code quality. It is a result of professionalism as a software engineer and has a direct effect on readability, code maintenance, and on the performance of both the individual who writes the code and the team as a whole.
It is worth noting, however, that years of experience are not a synonym for competence. There are professionals with many years in the field who still operate in an amateur way, with a level of carelessness that compromises the quality of their solutions. In many cases, those solutions are created to solve one problem but end up dragging in several others that never get addressed properly.
That said, this does not mean that a good professional is immune to mistakes. On the contrary: an experienced software engineer actively seeks feedback, with a mindset of failing fast and correcting quickly. What I am pointing at here is the behavior of inexperienced, amateur, or careless professionals, which unfortunately exists in any field.

Vibe coding
Vibe coding is a movement that will likely increase the responsibilities of more experienced software engineers. Beyond reviewing code written by other developers, they will also need to review code generated by AI tools, which in the future may be produced by teams outside the technology department, such as commercial or marketing teams.

These AI tools are currently effective as support, but they do not yet have the maturity to fully replace a technology professional. They can be extremely useful for accelerating processes or assisting with specific tasks, but they should not be treated as the primary tool for software development. Critical review and the ability to deeply understand the context of a problem are skills that, for now, remain exclusive to experienced professionals.
Beyond that, there is an important point: skills that are not constantly exercised tend to deteriorate over time. When not used, they lose their sharpness, which can negatively impact the quality of work. In a scenario where AI tools are growing but still depend on human supervision and intervention, it is essential for software engineers to keep their skills sharp and up to date, so that technological evolution becomes an opportunity for growth rather than a threat.
"We are what we repeatedly do. Excellence, then, is not an act, but a habit." — Aristotle
Choices shaped by context
Most of the guidance I will share here is directed at a corporate environment, where the focus is not on optimizing machine resources due to technical constraints, such as IoT devices, or specific performance objectives. As I have mentioned before: there are no unique solutions, only trade-offs. These guidelines apply to large-scale projects with frequent changes in both business requirements and team composition.
In that context, the main qualities these practices aim to promote are: readability, modularity, changeability, and maintainability. These quality requirements are essential to keeping code sustainable and adaptable over time.
Do not use comments
Good code is its own documentation, and I think that sentence already makes the intention clear. Becoming proficient at writing code clearly and efficiently is a skill every good software engineer needs. Learning to code well is just as important as understanding architecture, design patterns, and best practices. Writing simply and directly, so that the code becomes its own documentation, improves readability and, as a consequence, maintainability.
"Comments are a failure to express yourself in code. Instead of writing a comment, write code that is better." — Martin Fowler
Comments not only increase the number of lines in a file, they create an extra effort to be skipped, and you will inevitably skip them. They also indicate that the code needs additional explanation to be understood. The only exception would be when documentation needs to be generated from comments, as in the case of a JavaDoc for a library. In today's world of APIs and MCPs, even that practice is becoming less and less necessary.
It is worth remembering that high-level languages are designed to be closer to human language. This means code is meant for people, and those languages have evolved to become increasingly simple and concise, as Swift and Kotlin show. That said, they do not resolve all the ambiguities of natural language. Writing good code is therefore an essential skill for a software engineer, in the same way that a writer has the craft to produce literary work.
Vague, generic, or context-free names
int data = 10; // what is 'data'? it could be anythingAbbreviated or context-free names drastically reduce comprehensibility, increasing the cognitive effort required to figure out what the variable represents, how it should be populated, whether it is optional, and what side effects might arise when it is modified.
// what is 'person data'? it could be anything
PersonData personData = new PersonData();
// what is 'person info'? it could be anything
PersonInfo personInfo = new PersonInfo();
// what object does 'o' represent
// and what does 'info' mean for the business as an attribute or behavior?
o.getInfo();These objects end up becoming a genuine "bag" of variables. The developer, not understanding the problem or the business domain deeply enough, creates generic objects that do not clearly reflect what they represent in the application's specific context. This happens because the developer often does not go deep enough into understanding the problem and treats variables superficially, without working toward an effective model.
This problem stems from a lack of business understanding or, often, from professional immaturity. When I talk about understanding the business or business modeling, I mean the knowledge of the domain expert: the person on the front line who has studied the domain, understands the processes, commands the specific language of the business, and knows the technical terms used in that context.
Eric Evans, in Domain-Driven Design, highlights the importance of getting close to the business expert and understanding how they think and speak. That expert is the one who truly understands and lives the business in the real world.
"The Ubiquitous Language is a language shared by all team members, developers, domain experts, and other stakeholders, who work together on the project. It must be used in all communications, both written and spoken, to ensure everyone has a common understanding of the domain." — Eric Evans, Domain-Driven Design: Tackling Complexity in the Heart of Software (2004)
For the ubiquitous language to be effective, it must be reflected in the interactions between the technical team and the business, in the naming of teams and tribes (or whatever organizational structure is in use), and in the definition of names for projects, packages, classes, and variables.
"The ubiquitous language connects domain models with code, ensuring that each concept has an expression in the software and that conversations about the software focus on business aspects rather than technical details." — Vaughn Vernon, Implementing Domain-Driven Design (2013)
Another point worth raising is that developers are becoming increasingly distant from business experts, something the signatories of the Agile Manifesto strongly emphasized. Modern models often introduce an intermediary role to bridge the business expert and the technical team. The problem is a shortage of professionals capable of understanding and translating business rules efficiently for the technical team. Information that arrives is frequently imprecise or incomplete, which directly impacts the refinement process. That impact shows up in the final solution and, as a consequence, in the generated code.
Names carry intent
Some naming details deserve attention. When declaring a primitive variable, the name is typically singular. For collections like lists or arrays, the plural form is used.
var person = new Person(/*...*/);
var people = new ArrayList<Person>();Only useful variables
Unused variables should be removed from the code, just like unnecessary imports. Extreme Programming (XP) emphasizes continuous refactoring as an essential practice to ensure software quality and simplicity. During refactoring, only necessary and tested code should remain, eliminating anything that adds no value or is no longer used — avoiding what is commonly called "dead code."
XP also reinforces the principle of simplicity, which argues that you should write only the code strictly necessary to solve the problem at hand. This includes eliminating any piece of code that is not used or does not directly contribute to software value, resulting in clearer, more efficient, and easier-to-maintain solutions.
"Simplicity is the art of removing what is not necessary. Complexity added unnecessarily to code only creates problems, not solutions." — Kent Beck, Extreme Programming Explained (1999)
Another principle that reinforces this is YAGNI (You Aren't Gonna Need It). It reminds us to focus only on what is strictly necessary, keeping the code simple and direct. We should not implement pieces of code based on the assumption that we will need them in the future. The intention is to use development time well, reducing the risk of investing effort in something that may never be useful. This contributes to code that is clearer, leaner, and free of assumptions that often turn out to be wrong.
This does not mean ignoring the need to design for future extensibility. Extensibility should be considered when there is a known, planned problem with a realistic implementation timeline. That is very different from premature overengineering. We should create designs with abstractions that support extensibility when needed, while avoiding implementations with no real predictability of use.
The point is: we should not create implementations without a real, foreseeable use case, whether now or in the near future. Doing so is often driven by developer ego and ends up generating unnecessary technical complexity. Though it may seem like a "good idea," this type of decision can negatively affect the project, making it harder for other developers to understand and adding weight to maintenance.
Back to variables, here is a simple example to illustrate. Suppose 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 creating a mask for LocalDate, I might as well create one for LocalDateTime 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 tell yourself: "I don't need this for the current task, but since I'm already creating the formatter for LocalDate, I might as well have it ready for LocalDateTime too." By doing that, you violated the principle. That formatter may never be used, and you already polluted the code with unnecessary parts.
A related principle is KISS (Keep It Simple, Stupid!), which proposes keeping code simple and organized. This means creating and using patterns when necessary, but avoiding variables, implementations, or complexity that add no real value to the software.
Declare only when needed
In the past, compilers were far less sophisticated than they are today, and a common strategy was to declare variables at the top of a method or function. This allowed the compiler to perform simpler and more predictable optimizations, such as allocating memory contiguously. It also helped the programmer have a clear view of all 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.
That approach had problems, however. Beyond the risk of the program entering an incorrect state by using a variable in the wrong scope, there was also the risk of memory leaks when the developer forgot to deallocate a variable that was no longer in use.
Today, that practice is no longer recommended. In complex situations, trying to remember the type of a variable, when or how it was initialized, can make the code hard to follow. To ensure more fluid and minimalist reading, it is better to declare variables only when they genuinely need to be initialized, since that increases clarity and reduces the risk of bugs related to improper variable use.
// avoid this kind of code
public Transaction process(...) {
LocalDate date;
Transaction transaction;
// ...
date = LocalDate.now();
transaction = new Transaction(..., date);
transaction = transactionService.save(transaction)
return transaction;
}
// this would be the recommendation
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 flow more naturally, prevents initialization mistakes in longer code blocks, and eliminates "clutter" such as unused variables left behind after logic changes or refactoring.
"Software is a great combination of art and engineering." — Bill Gates
Another thing to avoid is declaring variables in a scope different from where they will actually be used. For example, a variable that should live inside a method but is declared outside it, holding memory longer than necessary. This increases the risk of problems related to memory management and unnecessary resource usage.
int sum = 0; // Variable declared outside the method
public void example() {
for (int i = 0; i < 10; i++) {
sum += i;
}
if (sum > 10) {
System.out.println("Sum is greater than 10");
}
}
// this would be correct
public void example() {
int sum = 0; // Variable declared in the smallest possible scope
for (int i = 0; i < 10; i++) {
sum += i;
if (sum > 10) {
System.out.println("Sum is greater than 10");
}
}
}Establish conventions
Establishing naming and architecture conventions in a project is essential. One of the most important areas to standardize is variable naming.
In a project I worked on, which was relatively new (and I want to be clear that "legacy code" does not necessarily mean bad code), the solution itself addressed an interesting problem. However, the execution in code was not particularly pragmatic. While trying to understand it, I ran into several difficulties, many caused by glaring issues. Here is an example of one problem I found:
data class Transaction(
val transactionId: UUID,
... // other variables
val idTransaction: UUID
)This class had two IDs. Nothing wrong with that in itself, but one represented the identity of the class and the other referred to the ID of another system. Figuring out which was which became a challenge. Initially, the team members could not explain why there were two variables, and the only way to understand which one to use was to search for where they were used or initialized.
It is not a complicated thing to investigate once you understand the context, but it becomes extra effort. I had to stop someone to get an explanation, investigate the variable usage, and only then focus on the solution. That generated a context switch and broke the flow of work. It makes the system harder to understand, increases the risk of errors, and reduces the productivity of anyone working on the code, while affecting overall team performance.
In Extreme Programming (XP), among the ten practices, the ninth is coding standards. The use of conventions and coding standards is essential to ensure that code is clear and easily understood by all team members.
Another example of convention is the format for variable naming. In the .NET world, PascalCase is widely adopted, where every word starts with a capital letter. In Java and Kotlin, the convention is camelCase, where the first letter is lowercase and subsequent words start with a capital letter.
Understanding these general conventions matters because, in Go, for instance, a public variable 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 across the codebase.
// Golang
package main
import "fmt"
type Person struct {
Name string // Public, starts with uppercase
age int // Private, starts with lowercase
}
func (p *Person) SetAge(i int) {
p.age = i // Private, accessible within the package
}
func (p *Person) GetAge() int {
return p.age // Private, accessible within the package
}
func main() {
p := Person{"João", 30}
fmt.Println(p.Name) // Accessing public variable
// fmt.Println(p.age) // Error! Cannot access private variable directly
p.SetAge(31)
fmt.Println(p.GetAge()) // Accessing private variable via public method
}In JavaScript, a popular convention for indicating private variables is to prefix them with an underscore (_). This signals that the variable should not be accessed directly from outside the class or module, even though JavaScript does not enforce encapsulation as strictly as other languages.
// JavaScript
class Person {
constructor(name, age) {
this._name = name; // Convention for private variable
this._age = age; // Convention for private variable
}
getName() {
return this._name; // Accessing private variable via public method
}
setName(name) {
this._name = name; // Modifying private variable via public method
}
getAge() {
return this._age;
}
}
const person = new Person("João", 30);
console.log(person.getName()); // Accessing via public method
person.setName("Carlos");
console.log(person.getName()); // Accessing via public methodWatch out for "accents"
A programmer who comes from another language often brings that language's "accents" along. This is why it is important to adapt not only to the conventions of the language but also to the conventions the team and company have already established.
For example, a .NET programmer building a project in Java might adopt PascalCase naming, open braces on a new line, and other patterns typical of .NET. When a Java developer needs to continue or maintain that code, the recommendation is to preserve the convention already in use, to avoid a clash of styles. It may be uncomfortable, but keeping the project consistent until the end is important.
If the team decides by consensus that conventions need to change, then the ideal is to refactor the entire project to the standard Java convention of camelCase. That is not always feasible, especially in large projects or tight deadlines.
Here are some basic conventions worth reviewing:
// names should be declarative and concise
Person person = new Person(...);
// the plural already indicates that people is a collection
List<Person> people = new ArrayList<>();
// some prefer prefixes like *is, has* for variables, I prefer using the name itself
// it is often self-explanatory
boolean active = false
// constants should always be uppercase in snake_case
public static final Integer INITIAL_VALUE = 10
// avoid generic type names in variable names
List<String> stringList;Suppress redundant suffixes and prefixes
Another common problem is increased verbosity in variable declarations. Remember that coding at high performance means minimizing reading effort. Excessive reading effort has a negative impact on productivity. The consequence of this excess verbosity is that it becomes harder to model and structure good design, which is one of the main concerns of good software engineers.
More is not always better. One example of this is when naming variables in ways that create unnecessary redundancy.
"Clean code always looks like it was written by someone who cares." — Robert C. Martin
// No need to repeat the class name or type in the variable
public class Transaction {
private UUID transactionId;
private LocalDate transactionDate;
private BigDecimal valueTransaction;
private LocalDateTime createdDate;
}Avoid repeating the class name or type in the variable name. The class name already defines a context, so repeating it in its attributes adds no meaning. In the Transaction class, all attributes already belong to that class, and repeating the class name in each attribute only increases verbosity without improving the model. Repeating the type name in variables does not make them easier to understand either. To avoid this, establish naming patterns for attribute types across the project.
What about when attributes of class A contain IDs or references to another class B? Should you use the other class name in the attribute name? I will leave that reflection for another post.
A more minimalist format would be:
public class Transaction {
private UUID id;
private LocalDate date;
private BigDecimal value;
// here you establish the convention that every 'created' field is a LocalDateTime
private LocalDateTime created;
}The obvious often needs to be stated. What is obvious to some is not always obvious to others. No matter how natural it may seem, these points often need reinforcement. Documenting conventions, creating an onboarding process that clearly communicates these guidelines, or offering coaching during pairing sessions in environments that promote good practices are all essential actions. They ensure continuity of quality maintenance and evolution within the team, and promote a shared language where everyone starts to think and write code in a similar way.
Another benefit of these conventions is the direct impact on variable usage. With good conventions in place, code becomes more fluid, with less text to read, which simplifies comprehension, provided the variables have well-chosen names.
// ...
if (transaction.getValue() > LIMIT_FRAUD_ANALYSIS) {
// ...
}
// ...Avoid long variable names
Remember: the goal is to reduce cognitive effort and increase readability. A very long variable name means more words to read and interpret, which increases the difficulty of understanding its purpose. See how an excessively long variable name can become tiresome and obscure its function:
// without a conventional standard
String thisIsALongTextToShowHowHardItCanBeToUnderstand = "Certainly"
String evenWithCamelCaseItBecomesDifficultToReadAndFollowLongNames = "Did you notice?"It is also worth noting that if you use tools like Checkstyle, Lint, or similar (and I strongly recommend it), they help statically validate whether your code follows community-recommended standards, contributing to code quality improvement.
For anyone who has never used these tools, they are also excellent for learning best practices and developing skills in writing code. In the example above, a variable with an excessively long name will likely exceed the character limit configured in those tools, which flag the violation. Limiting the number of characters per line improves readability, standardizes code across all developers on the team, encourages clearer descriptions of variables, and eliminates horizontal scrolling in the editor, especially on monitors that are not widescreen.
Avoid generic and ambiguous types
Providing clear types in code helps convey the intent and meaning 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.
Good object-oriented or functional modeling is important, but if you declare variables with vague names made up of letters and numbers, you are making the code harder to read, understand, and maintain.
Avoid using objects that have no clear characteristics directly related to the business or the problem you are solving. Here is an example:
Object person = new Employee(...);
List<Object> people = new ArrayList();Even though the variable name makes it clear what the object is expected to be, using a generic type means it can be overwritten on the next lines with any other type. The variable loses its identity and can become anything, which in a large and complex codebase increases the risk of errors. We should avoid this kind of practice.
How can we keep something generic without losing the essence of the purpose? The answer is to use an abstract class that, while generic, limits the types that can be assigned to it. For collections, generics serve that purpose.
This approach also weakens one of the great advantages of strong typing: compile-time verification. It can lead to runtime errors and break the Liskov Substitution Principle (LSP), one of the foundational principles of object-oriented design.
The LSP states that objects of a derived class must be substitutable for objects of the base class without altering the expected behavior. Failing to guarantee this creates an architecture where system behavior is neither predictable nor safe.
Person person = new Employee(...);
List<Person> people = new ArrayList();Avoid magic numbers
Let us reinforce the mantra: we should write code that is easy to read and understand. Imagine you are inside a for loop and, out of nowhere, there is a calculation where you take a variable value and multiply it by a number that appears "from thin air" in the middle of the code. No matter how obvious that number may seem, it interrupts the reading flow. When reading the code, you suddenly need to interpret a number to understand its meaning, which increases cognitive complexity.
To improve readability, replace those numbers with variables or constants with descriptive names. This transforms the code into something clearer and more readable, where everything flows as text, and the meaning of each value is self-evident. The code becomes more self-explanatory and easier to maintain.
// notice that everything is text, but in the middle there is a number you need to interpret
public double area(int radius) {
return 3.14159 * radius * radius;
}
// notice how much more natural it reads when everything flows as text
public double area(int radius) {
return PI * radius * radius;
}If the value is reused in other places, consider moving it to a variable in an appropriate class to centralize its usage. If the value is specific to that context, make it a private constant within the appropriate scope.
Conclusion
We have covered quite a lot just about variables, and the topic of good naming practices is not exhausted. There is more to explore in future posts.
Code quality is not determined only by the functionality it provides, but by its readability, simplicity, and capacity for long-term maintenance. A good software engineer knows that well-written code does not need external explanations, because it documents itself through good practices and design choices.
Adopting clear conventions allows the developer to contribute directly to a more efficient and collaborative development environment. Maintenance and evolution of code become more agile when all team members are aligned around a shared language and a comprehensible structure.
Principles like KISS (Keep It Simple, Stupid!), YAGNI (You Aren't Gonna Need It), and LSP (Liskov Substitution Principle), among others, must be observed to ensure that code is not only functional, but sustainable. Tools like linters and checkstyle can help enforce these practices, promoting cleaner programming and fewer redundancies.
In the end, well-written code goes beyond implementing features. It reflects professionalism, command of the problem domain, and the ability to communicate complex solutions clearly and simply. Investing in good coding practices improves not only the final product, but also the environment and performance of the team as a whole.
In the next article, we will explore some scenarios to avoid in order to keep code clean, concise, and easy to maintain.