undo
Go Beyond the Code
arrow_forward_ios

Common Pitfalls in Full Stack Technical Challenges: A Backend Perspective

Angel Tomas Concha Layme
Software Engineer & Solver
October 2, 2025
To learn more about this topic, click here.

Abstract

This article highlights a series of common mistakes observed in full-stack technical challenges, with a focus on backend development using Java and Spring Boot. Based on the review of numerous submissions from candidates, it identifies frequent issues such as poor separation of concerns, using entities directly instead of DTOs, unclear naming, unnecessary or outdated documentation, improper error handling exposed to clients, confusion between authentication and authorization, and poorly defined REST endpoints. The goal is to provide a clear and practical guide to help developers avoid these mistakes and adopt sound practices early in their careers.


1. Introduction

In the context of full-stack technical challenges, particularly those focused on building APIs with Java and Spring Boot, common backend mistakes are inevitable. These issues, often seen in junior developers, can compromise the clarity, maintainability, and robustness of the application. In this article, I analyze some of the most frequent problems observed when reviewing such solutions: from tightly coupled designs to poor error handling and improper layer organization. The goal is to provide a practical guide to help developers recognize and avoid these common pitfalls and adopt sound practices early in their careers.


2. Direct use of entities instead of DTOs

A common mistake is to expose JPA entities (domain objects or database models) directly through the REST API instead of using DTOs (Data Transfer Objects). A DTO is a simple object whose purpose is to carry data between application layers or to the client, avoiding exposure of internal domain details. Using entities directly in responses might seem more straightforward at first, but it has several significant disadvantages:

This record acts as a secure, immutable data container. Its fields are final, and it has no setters, which eliminates the risk of accidental mutations after construction. This approach simplifies reasoning about the code, reduces bugs related to shared mutable state, and aligns well with the principles of functional programming and clean architecture. Before Java records, developers often relied on plain classes with private final fields and only getters, or on tools like Lombok with annotations such as @Value to achieve immutability.

Here's a practical example using a Note entity and a tailored NoteDTO



            


In the controller layer, the mapper is used to transform the entity into a DTO before returning it to the client:



            


As shown, the client receives a NoteDTO that contains only the necessary and properly formatted information, rather than the complete Note entity. This approach improves:

For instance, we could later add a summary field to the NoteDTO or rename authorName to createdBy, without affecting the database, the business logic, or other consumers of the Note entity internally.

3. Unclear names in code

Another common pitfall is choosing ambiguous or poorly descriptive names for classes, methods, variables, and even endpoints. Phil Karlton once said that "naming things" is one of the two most complex problems in computer science, and it’s true. A good name can make code read almost like pseudocode, while a vague name can hide the code's intent and make it harder to understand.

I've seen candidates use generic names like processData(), handler, obj, tmp, and the like, which don't indicate what they do or contain. This forces the reader to guess or read the full implementation to understand it, increasing cognitive load. Keep in mind that developers spend more time reading code than writing it, so readability is key.

Some examples of unclear names and how to improve them:


A principle I follow is that a good name should make a comment unnecessary. If you are tempted to write a comment explaining what a variable or method does, consider whether a more descriptive name would suffice instead. For example, instead of:



            


We could rename the method to isUserAdult(int age), and the comment becomes redundant; the intent is clear just from the name.

In collaborative environments, clear naming improves communication and reduces errors. It helps new team members understand the code faster. Avoiding cryptic or generic names is essential: every identifier in your code is an opportunity to express developer intent. Choosing descriptive and consistent names is an investment in the quality and maintainability of the project.


4. Unnecessary documentation and comments

Documentation in code is an area where balance is delicate. On one hand, well-placed comments can clarify intentions or complex design decisions. On the other hand, overusing comments, especially those that explain the obvious, can generate noise and even confusion. A typical mistake among novice programmers is to comment excessively on every line or block, thinking it will make the code easier to understand, but this can actually have the opposite effect if the comments don't provide substantial information.

A mantra from Clean Code is: "A comment is a failure." Robert C. Martin exaggerates a bit to make a point, but if we need to comment on something, it means the code is not self-explanatory. Ideally, the code should be clear by design, using meaningful names (as discussed earlier) and proper structure. When this is achieved, redundant comments become unnecessary.


4.1 Examples of unnecessary or harmful comments



            


This comment adds no value; any programmer understands the +1 operation. Comments like these only clutter the code with superfluous text. Another typical example is commenting on attributes with generic phrases:



            


The variable name already conveys that. The comment is pure noise.



            


This kind of standard documentation adds no insight (it's evident that getName() returns the name) and only bloats the file. Extensive documentation should be reserved for explaining why something is done or business-related details, not what each trivial line does when the code is already clear.



            


This commented-out block is counterproductive. If the code is no longer used, it should be removed. That's what version control systems (like Git) are for: they allow us to recover history if needed. Leaving dead code only creates doubt; a future reader might wonder if the commented-out logic is still valid or was just forgotten. It's better to rely on Git to see past changes and keep the source clean of inactive sections.



            


Then the "Description" is either blank or says “Class for X”. Such boilerplate comments are unhelpful and usually end up forgotten. Unless the team or project explicitly requires author/license annotations, it’s better to omit them.


4.2 When to comment

There are situations where a comment is helpful or necessary:



            



            



            


Documenting code is only valid when it adds information not evident from the code itself. Before writing a comment, consider whether the code could be refactored to explain itself better (by renaming, extracting functions, etc.). A good comment never describes what the code does; that should be visible in the code itself, but rather why it does it, or anything that cannot be easily deduced from the implementation. Keeping unnecessary comments out of the code makes it cleaner and easier to maintain.


5. Inadequate error exposure to clients

In web applications, errors are inevitable: invalid inputs, missing resources, server failures, and so on. A common mistake is how those errors are handled and exposed through the API to the client. Especially in early stages, a critical error often made is to propagate the exception or its message directly to the client, which can result in confusing responses or even expose sensitive backend information.

For instance, imagine our Spring Boot API receives a malformed request. Without proper handling, Spring Boot will generate a default error response (the "Whitelabel Error Page" in HTML or its JSON equivalent). These default responses include technical details intended to assist developers, but are not helpful and potentially harmful for API consumers.

A default Spring Boot JSON error response might look like this:

Note: In Spring Boot 3.x, fields like exception or trace are not included by default. Their presence depends on server.error.* settings (e.g., server.error.include-exception, server.error.include-stacktrace). The JSON below is illustrative.



            


Several issues immediately stand out in this auto-generated error:

In general, stack traces or raw exception messages should never be sent as-is to the user. This is a bad practice for two key reasons: first, security (an attacker could infer information about the server, class structure, etc., from these details); and second, API usability (clients receive messages they cannot interpret). As Bruno Leite points out, Spring Boot’s default error traces can be helpful during debugging, but are useless to regular API consumers.


What’s the best practice?

In Spring Boot, it’s recommended to catch exceptions and convert them into clean, concise, and properly formatted responses (e.g., an error JSON with clearly defined fields: error code, user-friendly message, etc.). There are multiple ways to do this:



            


In this example, ErrorInfo would be a simple DTO with fields like message and timestamp. The first handler maps a missing resource exception to a 404 with a clear message. The second is a catch-all for unexpected exceptions, ensuring even unhandled errors don’t leak sensitive details (detailed log on server, generic message for client).

Additionally, a well-designed REST API returns appropriate HTTP status codes for each error type:

Optionally, include an application-specific error code or a link to documentation. But always avoid exposing full stack traces. The developer experience on the client side improves dramatically when error responses are clean and expressed in business terms (e.g., "message": "Note title is too long; maximum 100 characters" instead of a NullPointerException).


6. Confusion between authentication and authorization

In the realm of application security, two fundamental concepts are authentication and authorization. Although they sound similar (and both are often abbreviated as "auth"), their meanings are different. Confusing them is a common misunderstanding in technical environments, even among professionals with limited security experience.

Confusion between these concepts shows up in various ways in technical challenges:


A phrase to remember it: "Authentication is checking who you are; Authorization is deciding what you are allowed to do." Authentication is a prerequisite for authorization: without identity, you cannot apply permission rules.

Spring Security provides clear tools for both layers. For example, you configure an AuthenticationManager to authenticate (usually by comparing username/password against a user store), and separately configure authorization rules to indicate which roles can access which URLs or methods (e.g., using @PreAuthorize, .hasRole("ADMIN") in the DSL configuration, etc.). Understanding the difference prevents errors such as attempting to validate roles before authenticating (which makes no sense, because without identity, the role is unknown) or returning incorrect messages. A common mistake is responding with “401 Unauthorized” when in fact the user is authenticated but lacks permissions (which should be a 403 Forbidden). Even the name of code 401 confuses, but semantically, 401 means lack of authentication, and 403 means lack of authorization.

In technical challenges, I would recommend being explicit: for example, if implementing JWT, make it clear what it verifies (token signature → issuer authentication). If you then restrict an endpoint to specific roles, that is, authorization: using method names like checkPermission() vs authenticate() can help avoid mixing concerns. And in the documentation or project README, use the terms correctly:

"The system implements authentication via JWT and role-based authorization for the different resources."

7. Sources

The observations and advice presented here are based on my review of numerous technical challenges, as well as literature and best-practice documentation. To further explore related topics, I recommend materials such as "Sustainable Code" and "Agile Design with TDD" by Carlos Blé, the official Spring documentation on error handling and security, and specialized articles on RESTful design and design and architecture patterns in Java. Every mistake is an opportunity for improvement; with continuous study and practice, these pitfalls can be turned into strengths in our development.

Angel Tomas Concha Layme
Software Engineer & Solver

Start Your Digital Journey Now!

Which capabilities are you interested in?
You may select more than one.
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.