The plan was straightforward:
Frontend
↓
Backend API
↓
PostgreSQL
Signup, login, JWT, refresh tokens, forgot password, reset password.
But getting all of it working exposed a chain of bugs that taught us much more about debugging than simply implementing the feature.
The First Crash: Duplicate Spring Beans
The backend wouldn't even start.
The first error was:
ConflictingBeanDefinitionException:
Annotation-specified bean name 'AIResponseParser'
conflicts with existing bean definition
There were two different classes with the same name:
com.investmentai.ai.util.AIResponseParser
com.investmentai.ai.parser.AIResponseParser
Both were registered as Spring components.
Instead of randomly deleting one, we searched the project for every reference:
Get-ChildItem src\main\java -Recurse -File -Filter *.java |
Select-String -Pattern "AIResponseParser"
That helped us identify which implementation was actually being used.
After resolving the duplicate, we tried again.
And got another duplicate bean.
Round Two: Another Duplicate Bean
This time:
ConflictingBeanDefinitionException:
Annotation-specified bean name 'tokenValidator'
There were two classes:
ai.security.TokenValidator
ai.validator.TokenValidator
They had completely different responsibilities, but Spring derived the same default bean name from both classes.
Again, we searched for usages before changing anything.
The lesson was simple:
Don't fix a startup error blindly. First understand why both components exist and where they're being used.
After resolving this conflict, the backend finally started.
🗄️ Next Problem: Database Configuration
The authentication system depended on PostgreSQL running through Docker.
We verified the container:
docker compose ps
Then verified the database directly:
docker compose exec postgres psql -U myuser -d mydatabase
And checked the tables:
flyway_schema_history
refresh_tokens
users
This helped separate the problem into two independent parts:
Application configuration
↓
Database connectivity
↓
Database schema
Instead of assuming the database was broken, we tested it independently.
That made debugging much easier.
🔐 Finally: Signup and Login
Once the backend was stable, we tested signup directly through the API.
POST /auth/signup
The user was successfully created.
Then:
POST /auth/login
returned a JWT access token.
We also verified that the user was actually stored in PostgreSQL.
At this point, the core authentication flow was working:
Signup
↓
PostgreSQL
↓
Login
↓
JWT
But authentication still had one major feature left.
Forgot password.
🔑 Building the Password Reset Flow
We created a dedicated password reset token table.
The important part of the design was that we didn't store the raw reset token in the database.
The flow was:
User requests password reset
↓
Generate random token
↓
Hash token
↓
Store hash in PostgreSQL
↓
Send raw token through email
↓
User clicks reset link
↓
Validate token
↓
Update password
We also tracked:
- expiration
- whether the token was already used
- the associated user
The database migration was applied successfully.
The reset link was generated correctly.
The reset page opened correctly.
Everything looked good.
Except...
the email never arrived.
📧 The SMTP Problem
We initially used Gmail SMTP.
The configuration looked normal:
spring.mail.host=smtp.gmail.com
spring.mail.port=587
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
But the backend returned:
535-5.7.8 Username and Password not accepted
This was interesting because the application could successfully connect to Gmail's SMTP server.
The failure was happening specifically during authentication.
We checked the environment variables.
We checked the App Password.
We checked the SMTP configuration.
We eventually realised that relying on a local .env file did not automatically make those variables available to the Spring Boot process.
That was another useful lesson:
A configuration file existing in your project doesn't necessarily mean your application is actually receiving those values.
🔄 Changing the Approach
We replaced:
Spring Boot
↓
Gmail SMTP
↓
Authentication failure
with:
Spring Boot
↓
Email API
↓
User's inbox
The important part was that we didn't rewrite the password-reset logic.
The existing service still did:
emailService.sendPasswordResetEmail(
user.getEmail(),
resetUrl
);
Only the implementation of EmailService changed.
This was a good example of why separating business logic from infrastructure matters.
The authentication service shouldn't care whether an email is delivered through SMTP or an API.
🎯 The Final Test
We triggered:
POST /auth/forgot-password
The backend:
- Found the user.
- Generated a reset token.
- Stored the token hash.
- Created the reset URL.
- Sent the email through the new email service.
📩 The reset email arrived.
The user clicked the link.
The reset page opened.
The password was changed.
The new password worked for login.
🧠 What This Bug Taught Me
The biggest lesson wasn't about Spring Boot, PostgreSQL or email APIs.
It was about debugging by layers.
Instead of looking at the entire authentication system as one big problem, we broke it down:
Frontend
↓
HTTP Request
↓
Spring Controller
↓
Authentication Service
↓
JWT / Token Logic
↓
PostgreSQL
↓
Email Service
↓
Email Provider
Then we tested each layer independently.
A problem that initially felt like:
"Authentication isn't working."
became several smaller, solvable problems:
- Duplicate Spring beans
- Database verification
- Migration verification
- Environment variable configuration
- SMTP authentication
- Email provider integration
And that made the debugging process much less overwhelming.
🏁 From Broken to Working
The final authentication flow became:
Authentication
│
┌────────────┼────────────┐
↓ ↓ ↓
Signup Login Forgot Password
│ │ │
↓ ↓ ↓
PostgreSQL JWT Reset Token
│
↓
Email API
│
↓
Reset Link
│
↓
New Password
│
↓
Login
The best part wasn't that we avoided bugs.
We didn't.
The best part was learning how to trace a failure, isolate the layer causing it, verify assumptions, and change only what actually needed changing.
Sometimes debugging isn't about finding one bug.
Sometimes it's about following the trail until the whole system finally makes sense.



Top comments (0)