# The Onboarding Ghost: Engineering a Java Email Bot from Nothing

> No welcome email. No confirmation. No system. When a new user signed up to Fixme, they were met with silence. This is the full story of how I built the machine that fixed it — from blank slate to a resilient, 9-class Java Spring Boot service that now forms the foundation of customer onboarding.

Canonical URL: https://francisj2nd.cv/cs/fixme-onboarding-system/
Category: Engineering / Java
Date: May 10, 2026
Read time: 20 min read

## TL;DR

Fixme had zero onboarding automation. I designed the user journey, built HTML email templates from scratch, prototyped a Node.js/Puppeteer scraper (and watched it break), then re-platformed the whole thing in Java 17 + Spring Boot. The final service features dual-SMTP failover, DNS-level email validation, loop-back pagination detection, persistent JSON deduplication, and per-record fault isolation — covering both individual users and business vendors automatically, 24/7.

Key metrics:
- New Users Covered: 100%
- User Segments: 2
- Hours Saved / Month: 15

---

When I joined Fixme, I quickly discovered a ghost in our machine. Not a bug, a performance issue, or a missing feature. It was an absence. A complete void where a critical business process should have been: customer onboarding.

When a new user signed up for our platform, they were met with absolute, deafening silence. There was no welcome email. No "getting started" guide. No confirmation of their registration. For the small businesses and vendors who spent considerable time meticulously building their profiles on our marketplace, this silence was even more damaging. They were left in a state of limbo, completely unaware if their submission was received, under review, approved, or simply lost to the digital ether.

This wasn't a minor flaw in the user experience; it was a fundamental business failure. We were investing heavily in marketing and acquisition to bring people through the front door, only to have them walk into an empty, unlit room.

But Fixme had no pre-existing system, no templates, and no tools for this. I realized then that I couldn't just write a proposal. I had to become the solution.

> I couldn't just write a proposal. I had to become the solution.

## Phase 1: Architecting an Experience from a Void

Before a single line of code could be written, I had to be a product manager and a UX designer. I began by diagramming the entire user journey from the first ad click to the final signup.

The onboarding gap was stark: Acquisition (ad click → landing page → signup form) → **The Void** (no confirmation, no welcome, no guidance — user value perception plummets) → Churn (user forgets, loses trust, never activates).

I designed a two-pronged strategic response tailored to our two primary user groups:

**Segment 01: Customer**
- Immediate activation and value discovery
- Visual quick-start guide
- Seed upgrade-to-business path

**Segment 02: Vendor**
- Validation, momentum, and trust
- Celebratory "launch kit" email
- Profile optimization tips + video link

A plain-text, system-generated email would feel cold and undermine the entire effort. I used Deepsite to meticulously craft two distinct, fully-responsive HTML email templates. Every color, font, and image was chosen to align with our brand identity and create a powerful, welcoming first impression.

The finished templates are live at:
- francisj2nd-user-welcome-email.static.hf.space — Customer welcome guide
- francisj2nd-business-approval-email.static.hf.space — Vendor launch kit

## Phase 2: The First Attempt — A Necessary, Fragile Prototype

To build momentum and prove the concept to my team, I needed a rapid proof-of-concept. I chose Node.js and Puppeteer — JavaScript's ubiquity and Puppeteer's high-level browser control API made it perfect for a working prototype fast.

The prototype worked. But as I ran it more frequently, I started living the developer's nightmare. The reliance on highly specific CSS selectors (`td:nth-child(10)`) was its Achilles' heel. A front-end developer on another team adding a single column in the admin UI would silently break my entire data-scraping logic.

The final straw: a user had signed up with the email address `blessed@com`. My email library rightfully rejected it as invalid, threw an exception, and because my error handling was a single, global try-catch block, the *entire process died*. Hundreds of valid users that came after that single bad entry were left in the onboarding void.

> One bad email address killed the whole batch. The prototype had to die.

The prototype was an immense success as a learning tool. It proved the value of the project and exposed every engineering challenge that a professional-grade solution would need to overcome.

## Phase 3: The Re-Platforming — Engineering for a Professional Java Ecosystem

The decision to migrate to Java was a commitment to robustness, maintainability, and alignment with Fixme's core infrastructure.

**Core Stack: Spring Boot**
- Java 17 + Spring Boot 3.5.3
- Playwright for Java v1.43.0
- Lombok — boilerplate elimination
- Google Gson v2.10.1 — state persistence

**Email Layer: Dual SMTP**
- JavaMailSender — primary (port 587)
- JavaMailSender — fallback (port 465)
- Brevo SDK v7.0.0 — trackable delivery
- DNS MX validation via JNDI

### Nine-Class Architecture

| Class | Role |
|-------|------|
| FixmeBotApplication | Spring Boot entry point |
| BotRunner | Main orchestrator: scraping, deduplication, processing |
| ScrapedUser | Immutable data model for scraped records |
| TemplateService | Loads HTML email templates from classpath resources |
| EmailService | SMTP delivery with intelligent dual-sender failover |
| EmailValidationService | Two-stage email validation (regex + DNS MX lookup) |
| MailConfig | Bean factory for primary and fallback JavaMailSender instances |
| PromotionalEmailService | Brevo API integration for trackable email delivery |
| StorageService | JSON file persistence for deduplication state |

## Phase 4: Engineering a Bulletproof Scraper and a Resilient System

### Dual-Target Scraping: Users and Businesses

`BotRunner` orchestrates two independent scraping runs per execution — one against the users table, one against the businesses table. Each new business owner gets the celebratory "Your Business is Now Live" email; each new customer gets the visual quick-start welcome guide.

### Solving Pagination with Loop-Back Detection

My solution: the bot collects the phone numbers from every row on the current page into a `Set`. After clicking "Next," it collects the phone numbers from the new page. If the two sets are identical, the admin panel has looped back to page one, which means the bot has seen everything.

This approach is fundamentally more reliable than timing heuristics because it validates the actual data content, not the browser's animation state.

### Two-Stage Email Validation

The `blessed@com` incident made it clear that a single regex check is not enough. The production `EmailValidationService` runs every email through two independent gates:

1. **Syntax check:** A strict regex validates the structure of the local part and domain, including TLD length (2–7 characters).
2. **DNS MX record lookup:** Even if the syntax is valid, the domain must have a real Mail Exchange record. A domain like `example.fake` will pass most regex checks but cannot actually receive email.

### Dual SMTP with Intelligent State-Based Failover

`MailConfig` provisions two completely independent `JavaMailSender` beans — primary on port 587 (STARTTLS) and fallback on port 465 (SSL/TLS). `EmailService` maintains a stateful `PreferredSender` enum that remembers which sender succeeded last. Network-level failures trigger automatic retry on the other sender; application-level rejections (invalid recipient) are not retried.

A mail server outage on port 587 will never silently drop emails. The bot detects it on the first failure, switches to port 465, and continues the batch without interruption.

### File-Based Memory and Deduplication

`StorageService` maintains two persistent JSON files: `users.json` and `businesses.json`. On startup, previously processed records are loaded. After scraping, only records whose phone numbers are absent from the stored set are processed. On completion, new records are merged into the master list using phone number as the deduplication key.

### Per-Record Fault Tolerance

The `blessed@com` experience taught me a vital lesson. Instead of a global try-catch that would bring down the whole application, each individual email send attempt is wrapped in its own try-catch block inside the processing loop. Any send failure is logged in detail without disrupting the batch.

### HTML Templates as Classpath Resources

Rather than embedding HTML as unwieldy string constants in Java code, the email templates live as proper files under `src/main/resources/templates/`. `TemplateService` loads them at runtime using Spring's classpath resource API.

### Admin Reporting and Test Mode

After every job run, `BotRunner` sends a structured summary email to the admin address. If the job completes successfully, the report details how many new users and businesses were found and processed. If the job throws an unexpected exception, a failure report is sent with the error details.

For safe testing without touching production users, the bot supports a `test-email` Spring profile that sends both email templates to the admin inbox using placeholder names.

### The Brevo Integration: Built for What Comes Next

`PromotionalEmailService` wraps the official Brevo SDK v7.0.0, providing a fully wired `sendTrackableEmail()` method that can swap in for SMTP delivery the moment we need open-rate analytics — no refactoring required.

> Adding open-rate tracking requires adding to the system, not rewriting it.

## The Final Result

**100% — Coverage:** Every new user and business receives a beautiful, personalized welcome within an hour of joining — 24/7

**0 — Repeat Emails:** Persistent JSON deduplication guarantees no existing member is ever emailed twice across any number of runs

**15h — Saved / Month:** Eliminated what would have amounted to 10–15 hours per month of manual, error-prone work

Ultimately, this project reinforced my core belief that the most impactful work often lies in solving the unglamorous problems that everyone else has accepted as a fact of life. By taking complete ownership — from business strategy and creative design, to deep engineering with Java 17, Spring Boot, and AI-assisted tooling — I was able to single-handedly build a solution where none existed.

I didn't just fix a leaky funnel. I engineered the system that ensures, for every single customer who joins our platform, their journey begins with a truly magic moment.

## Skills Demonstrated

Java 17, Spring Boot 3.5.3, Playwright for Java, JavaMailSender, dual-SMTP failover, DNS MX validation (JNDI), Gson JSON persistence, Brevo SDK, strategy pattern, per-record fault isolation, Puppeteer (prototype)

## Author

Francis Jeremiah Sharon  
https://francisj2nd.cv  
me@francisj2nd.cv  
https://www.linkedin.com/in/francisj2nd/
