Inside `jobportal`: How One Spring Boot App Splits into Two Products

A role-aware dashboard, shared identity model, and file-based uploads make this job portal more architecturally interesting than it first appears.

8 min read • View on GitHub • More from pandit2512

A single civic-style building with one front door marked login, then two staircases splitting inside. One side contains a job seeker desk with a résumé, saved listings, and a message tray. The other side contains a recruiter desk with job posts, applicant cards, and a hiring queue. It explains how one account can become two distinct workflows.
One entry point, two experiences. The app routes a shared identity into role-specific workspaces.
Key Takeaways

One Login, Two Dashboards

Most job portal demos start with the obvious part: post a job, search a job, apply to a job. This repo starts somewhere better. It treats identity as the product, then lets the dashboard branch from there.

That choice matters because the app does not need duplicate login screens or separate entry points for each persona. A job seeker and a recruiter share the same authentication spine, then land in different experiences after login. The result is simple on the surface and surprisingly disciplined underneath.

The routing layer and the data model work together. One account becomes two product paths without splitting the application into separate logins.

The Data Model Is the Real Product

The most elegant part of the repo is the schema. Instead of inventing separate user systems, it uses one `Users` entity as the base identity, then extends that identity with role-specific profile tables. That is a better fit for the problem than a loose pile of role flags.

The key move is `@MapsId`. It ties the profile primary key to the same identifier as the parent user row, so the profile is not a floating side record. It is a true one-to-one extension of the account. In practice, that makes the data model cleaner to reason about and harder to drift out of sync.

A mechanical close-up of a central Users gear with two profile plates bolted onto the same shaft. One plate represents a job seeker profile, the other a recruiter profile, and the shared shaft is marked by the same primary key relationship. Thin pipes branch outward to applications, job posts, and file folders. It explains how one identity can support multiple role-specific tables without duplication.
`@MapsId` turns the user table into a shared spine instead of a duplicate record factory.
PatternWhat this repo doesWhy it helps
Shared identityOne `Users` table anchors both personas.It keeps authentication and authorization in one place.
Role extensionSeparate job seeker and recruiter profile entities.Each persona gets its own fields without bloating the base user row.
Primary key mapping`@MapsId` reuses the user ID as the profile ID.The relationship stays tight and referentially clean.
AlternativeOne giant user table with role flags and nullable columns.That would be easier to start, but messier to maintain.

How Spring Security Triage Works

The security setup is doing quiet, useful work. Public routes bypass authentication, then a custom authentication success handler inspects the logged-in user and sends them to a shared `/dashboard/` endpoint. That route is not a dead end. It is a fork in the road.

This is a nice compromise between simplicity and flexibility. The app avoids scattering role logic across multiple login pages, but it still gives each persona a focused surface after authentication. The page may look unified, yet the experience is conditional by design.

That is the real trick here: role awareness happens after login, not before it. The result is a system that is easier to teach, easier to navigate, and less brittle than a hand-built maze of separate auth flows.

Search Is a Filter Bundle, Not a Simple Query

The search controller is a good example of the repo's overall style. It does not pretend job search is one text box and a database lookup. It accepts a high-density bundle of request parameters, then hands that bundle to the service layer for real filtering.

That means the controller is acting like a translator. Form inputs become a search contract. If nothing is set, the app returns everything. If filters are present, the service takes over. The shape is closer to a specification engine than a naïve query endpoint.

Search approachWhat happensTradeoff
Single keyword fieldOne query, one filter.Easy to build, weak for real job search.
Filter bundleMany request params flow into a service search method.More code, but much better control over relevance.
Hardcoded branchesSpecial cases in the controller.Fast to prototype, painful to extend.
This repoThin controller, heavier service logic.The right place for complexity.

Applications Are State, Not Just Submissions

The apply flow makes the same point from a different angle. Applying is not just inserting a row. The controller checks prior state, such as whether the user already applied or already saved the job, then creates a new application record with user context and a timestamp.

That matters because it turns the app into a history machine. A candidate is not just a guest clicking buttons. They accumulate state over time. Saved jobs, applications, profile data, and activity records all shape what the dashboard can show next.

This is where the design becomes coherent. Search discovers jobs. Saving marks intent. Applying records commitment. The system is not random CRUD. It is a small workflow engine wrapped in a familiar portal.

A lot of portfolio projects stop at form submission. This one goes one step further and asks, what should the system remember about the user after the click? That is a much more interesting question.

ActionStored signalWhy it matters
Save jobCandidate interestLets the dashboard remember intent.
Apply to jobFormal application recordCreates a durable hiring artifact.
Profile updateIdentity detailsFeeds matching and presentation.
Job posting activityRecruiter-side stateSupports management and review.

The File System Tradeoff

Uploads live in a local `photos/` directory, including resumes and profile images. That is a practical choice for a portfolio project because it keeps setup easy and avoids cloud dependencies.

The cost is obvious too. Local files are simple on one machine and awkward across multiple instances. They are fine for learning and demos. They are not the final word on durability or scale.

Storage choiceBenefitCost
Local `photos/` folderFast to run and easy to inspect.Hard to scale horizontally.
Database BLOBsKeeps files close to records.Can make the database bulky.
Object storageBest fit for production scale.Adds another service to configure.

That tradeoff does not weaken the project. It clarifies it. You can see exactly where the code stops being a teaching tool and starts becoming production infrastructure.


Why This Feels Like a Strong Portfolio Project

This repo succeeds because it is more than a list of features. It demonstrates layered Spring architecture, role-aware security, JPA relationships, and a dashboard model that reflects how real software separates shared identity from role-specific behavior.

Compared with the average job portal demo, it has a sharper idea. Many projects duplicate paths for each persona or bury role logic in the UI. This one keeps a single spine and lets the system branch where it should.

DimensionTypical job portal demo`jobportal`
Identity modelLoose role fields and duplicated logic.One shared `Users` core with profile extensions.
Login flowSeparate entry points or shallow redirects.One login, one dashboard, role-aware routing.
State handlingMostly form submissions.Applications, saves, and activity become durable state.
Engineering valueFeature checklist.A clear architectural pattern worth learning from.

That is why the repo stands out. It is educational, but not toy-like. It gives you a clean example of how to structure a Spring Boot app around identity, state, and role-based experiences without overcomplicating the surface area.