Spring Boot fresher rounds check whether you can explain the basics in your own words: what a bean is, how an app starts without a separate server, how a request reaches your controller, and how an entity becomes a table. Expect a small endpoint to write on a whiteboard and questions about the project you built in college. It is written for final-year students, new graduates and anyone coming out of an internship or a bootcamp who is facing a first Spring Boot interview. Each question shows what the interviewer is checking, the shape of a good answer and a short answer you can say out loud. Swap the project stories for your own.
Search all questions by round, difficulty and level, or save the ones you want to practice.
The container: the ApplicationContext creates, wires and keeps your objects.
A bean: any object the container manages, found through annotations or @Bean methods.
Why it matters: you ask for what you need, and the container hands it over already built.
“The Spring container, the ApplicationContext, is basically an object factory that runs when the app starts. It scans my packages, finds classes marked with things like @Service or @Repository, creates one object for each, and connects them to each other. Each object it manages is called a bean. The difference from a normal object is ownership. If I write new StudentService myself, I have to create its repository too, and Spring knows nothing about it, so things like transactions won't work on it. If it's a bean, I just ask for it in a constructor and the container gives me a ready one. That's what inversion of control means: my code no longer controls creating its dependencies, the framework does.”
Saying a bean is just a Java class with getters and setters, which mixes it up with a JavaBean.
One instance: singleton scope is the default, one object for the whole app.
Shared by threads: every request runs on its own thread and uses that same object.
The fix: keep services stateless; use local variables, method parameters or thread-safe types.
“By default Spring creates just one object of each bean, because singleton is the default scope. That one StudentService is shared by every request, and each request runs on its own thread from Tomcat's pool. So if I keep the current user in a field, two people logging in at the same moment can overwrite each other, and one might see the other's data. A counter in an int field has the same problem. count plus plus isn't atomic, so two threads can read the same value and one update gets lost. The fix is to keep services stateless: per-request data goes in local variables or method parameters. If I really need a shared counter, I'd use an AtomicInteger, or better, store it in the database.”
Saying Spring creates a new service object for every request.
The cause: injection is by type, and now two beans match.
@Primary: marks the default one when nobody asks for a specific bean.
@Qualifier: names the exact bean at the injection point; or inject a List of all of them.
“Spring injects by type. My controller asks for a NotificationService, and now there's an EmailNotificationService and an SmsNotificationService, both beans. Spring can't guess, so it refuses to start, which is better than silently picking one. There are a few fixes depending on what I want. If email is the normal choice, I put @Primary on that class, and anyone asking for the interface gets email. If one class really needs SMS, I add @Qualifier with the bean name, which by default is the class name starting with a small letter, so smsNotificationService. And if I want to send through every channel, I inject a List of NotificationService and Spring hands me all of them.”
Fixing it by deleting one class or by creating the object with new.
@Component: for your own classes; Spring finds them by scanning.
@Bean: for classes you can't edit, like a library's, or that need setup code.
Example: a RestTemplate, an ObjectMapper or a password encoder.
“For my own classes I just add @Component or @Service and component scanning picks them up. But I can't put an annotation on a class from a library, like RestTemplate or BCryptPasswordEncoder, because I don't own that source code. That's where @Bean comes in. I write a method inside a class marked @Configuration, create and set up the object there, and return it. Spring calls the method once and registers the result as a bean, named after the method by default. It's also handy when building the object needs some logic, like setting timeouts or choosing between two implementations. In my project I had a @Bean method for the password encoder, and then any service could ask for a PasswordEncoder in its constructor.”
@Configuration
public class AppConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Not knowing @Bean exists, or thinking you can annotate library classes.
Initializr: build tool, Java version, group and artifact, and the dependencies you need.
What you get: main class, application.properties, a test folder and the build file.
Run it: the wrapper script or the IDE, then check the startup log.
“I use Spring Initializr, either the website or the one built into my IDE. I pick Maven or Gradle, the Java version, a group and artifact name, and add dependencies, usually Spring Web, Spring Data JPA, a database driver and Validation. It gives me a zip with a clear layout. Under src/main/java there's the main class with @SpringBootApplication and a main method. Under src/main/resources there's application.properties, plus static and templates folders if I'm serving pages. There's a matching src/test/java with one test that checks the context loads. At the top is the pom.xml and the Maven wrapper, so anyone can build it without installing Maven. I run it with the wrapper or from the IDE, and the log tells me Tomcat started on port 8080.”
Having no idea where configuration files go, or never having created a project from scratch.
Embedded server: the web starter brings Tomcat in as a normal library.
Executable jar: the build plugin packs your code and every dependency into one jar.
Contrast: older apps built a war and copied it into a Tomcat you installed yourself.
“When I add the web starter, Tomcat comes in as a regular dependency, just like any other library. So instead of my app being deployed into a server, the server lives inside my app. When main runs, Spring Boot starts that embedded Tomcat on port 8080 and registers my controllers with it. The Spring Boot Maven plugin then builds an executable jar, sometimes called a fat jar. It holds my compiled classes plus every dependency jar inside it, and a small launcher that knows how to load them. So java -jar is all you need on the machine, apart from Java itself. The old way was to build a war file and copy it into a Tomcat someone had installed and configured on the server.”
Thinking Tomcat must be installed on the machine, or not knowing what a jar contains.
Port: server.port in application.properties or application.yml.
Database: the spring.datasource properties for URL, username and password.
Keep secrets out: real passwords come from environment variables, not the committed file.
“Both go in application.properties under src/main/resources. For the port I set server.port, say to 8081, if something else is already using 8080. For the database I set spring.datasource.url with the JDBC URL, then spring.datasource.username and spring.datasource.password. With the JPA starter I sometimes add spring.jpa.show-sql while learning, so I can see the SQL Hibernate runs. Spring Boot reads this file at startup and uses it to build things like the connection pool for me, so I don't write that code myself. One thing I learned in my project: the password shouldn't sit in a file that goes to GitHub. Spring Boot also reads environment variables, so for anything real the password comes from there and the file only has safe local values.”
server.port=8081
spring.datasource.url=jdbc:mysql://localhost:3306/college
spring.datasource.username=app_user
spring.datasource.password=local-only
spring.jpa.show-sql=true
Hard-coding the database URL and password inside a Java class.
Port in use: another process, often an old run of your own app, holds 8080.
DataSource: the JPA starter is present but no database URL or embedded database is set.
Habit: read the last error in the log first; it usually says the fix.
“The first one means something is already listening on 8080. Most of the time, for me, it was my own app still running from an earlier run in another terminal or IDE window. I either stop that process or change server.port to a free port. The second one appears when the Data JPA starter is on the classpath. Spring Boot then tries to set up a database connection, but it can't find spring.datasource.url and there's no embedded database like H2. The fix is to add the URL, username and password, or add H2 while I'm just learning. If I added JPA by accident and don't need a database yet, I remove that dependency. In both cases the log says what's wrong, usually near the bottom, so I read that before searching online.”
Restarting the laptop without reading the error, or not knowing where the log is.
| @Controller | the return value is a view name, rendered by a template engine. |
|---|---|
| @RestController | @Controller plus @ResponseBody, so the return value is written as JSON. |
When: server-rendered pages versus an API for a frontend or mobile app.
“With @Controller, a method usually returns a String, and Spring treats it as the name of a view. So returning home makes a template engine like Thymeleaf render home.html. @RestController is @Controller and @ResponseBody combined. Now whatever the method returns is written straight into the response body, and for an object that means Jackson turns it into JSON. In my college project the backend served a React frontend, so everything was @RestController and returned objects that became JSON. If I were building server-rendered pages, like an admin screen with forms, I'd use @Controller. You can also mix: a plain @Controller with @ResponseBody on one method returns data from just that method.”
Saying they are the same, or not knowing what @ResponseBody does.
| @PathVariable | part of the path itself, usually an id that names one resource. |
|---|---|
| @RequestParam | the query string after the question mark, often for filters or paging. |
Missing values: request params are required by default; use required false or a default value.
“A path variable is part of the URL path, like the 42 in /students/42/courses. It usually identifies one thing. A request param comes from the query string after the question mark, like ?semester=5, and it's used for filtering, sorting or paging. In the method, @PathVariable binds the id from the path, and @RequestParam binds semester from the query. One detail people miss is that @RequestParam is required by default, so if the client leaves it out, Spring answers with 400 Bad Request. If it's optional, I either set required to false and accept a null, or give it a defaultValue. Spring also converts types for me, so the id arrives as a Long, and a value like abc gets a 400 instead of reaching my code.”
@GetMapping("/students/{id}/courses")
public List<CourseDto> courses(@PathVariable Long id,
@RequestParam(defaultValue = "1") int semester) {
return courseService.findForStudent(id, semester);
}
Swapping the two, or not knowing that a missing required param returns 400.
Base path: @RequestMapping on the class, plural noun like /students.
Four verbs: GET to read, POST to create, PUT to replace, DELETE to remove.
Status codes: 200 for reads and updates, 201 for create, 204 for delete with no body.
“I put @RestController and @RequestMapping("/students") on the class so every method shares the base path. GET on /students lists them and GET on /students/{id} fetches one, both returning 200. POST on /students creates a student from the JSON body, and I return 201 Created, because that tells the client something new exists. PUT on /students/{id} replaces that student's details and returns 200 with the updated record. DELETE on /students/{id} removes it and returns 204 No Content, since there's nothing to send back. The controller stays thin: each method just calls the service. If an id doesn't exist, the right answer is 404, which I'd handle with an exception mapped to that status rather than returning null.”
@RestController
@RequestMapping("/students")
public class StudentController {
private final StudentService service;
public StudentController(StudentService service) { this.service = service; }
@GetMapping("/{id}")
public StudentDto get(@PathVariable Long id) { return service.get(id); }
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public StudentDto create(@RequestBody StudentDto dto) { return service.create(dto); }
@PutMapping("/{id}")
public StudentDto update(@PathVariable Long id, @RequestBody StudentDto dto) {
return service.update(id, dto);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable Long id) { service.delete(id); }
}
Using GET for everything, or returning 200 with an error message inside the body.
findById: returns an Optional, never null.
ResponseEntity: lets you choose the status and body in the method.
Map or else: found becomes 200 with the body, empty becomes 404.
“The repository's findById gives me an Optional, so I don't need a null check. I map the found student to a DTO wrapped in ResponseEntity.ok, which is a 200 with that body. If the Optional is empty, orElse gives back ResponseEntity.notFound().build(), which is a 404 with no body. The mistake I made early on was calling get on the Optional directly. When the id didn't exist it threw an exception and the client got a 500, which wrongly says the server broke. Another clean way is to throw ResponseStatusException with NOT_FOUND, or a custom exception that a global handler maps to 404. I'd use that once the app has many endpoints, so every missing record looks the same.”
@GetMapping("/students/{id}")
public ResponseEntity<StudentDto> getStudent(@PathVariable Long id) {
return studentRepository.findById(id)
.map(s -> ResponseEntity.ok(StudentDto.from(s)))
.orElse(ResponseEntity.notFound().build());
}
Calling Optional.get without checking, or returning 200 with a null body.
Filter: servlet level, runs before Spring MVC, sees every request.
Interceptor: inside Spring MVC, runs around the controller method and knows which handler it is.
Choice: a filter for all traffic; an interceptor when you need controller details.
“A Filter belongs to the servlet layer. It runs before the request even reaches Spring's DispatcherServlet, so it sees everything, including requests that never match a controller. A HandlerInterceptor lives inside Spring MVC. It runs after the DispatcherServlet has picked the controller method, with preHandle before the method and afterCompletion once the request is done, and it knows which handler was chosen. For timing every request, I'd use a filter. I note the time, call chain.doFilter to let the request go through, then log the URL, status and time taken. If I needed to know which controller method handled it, or I wanted to check something only for certain endpoints, I'd pick an interceptor and register it through WebMvcConfigurer. Spring Security itself is built on filters, which is why it can block a request before any controller runs.”
Saying they are the same thing, or putting the timing code inside every controller method.
@Entity: this class maps to a table; it needs a no-argument constructor.
@Id and @GeneratedValue: the primary key, and who creates its value.
@Column: column name and rules such as not null or unique.
“@Entity tells JPA that this class maps to a table, by default one named after the class. Every entity needs a primary key, marked with @Id. @GeneratedValue says I won't set the id myself. With the IDENTITY strategy the database creates it, like an auto-increment column in MySQL. @Column is optional, since every field maps to a column anyway, but I use it to set rules, like nullable false for the name or unique true for the email. JPA also needs a no-argument constructor, because Hibernate creates the object first and then fills in the fields from the row. In my project I used jakarta.persistence imports, since newer Spring Boot versions moved from javax to jakarta.”
@Entity
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(unique = true)
private String email;
protected Student() { } // required by JPA
public Student(String name, String email) {
this.name = name;
this.email = email;
}
}
Forgetting the primary key, or thinking @Column is required on every field.
What you inherit: save, findById, findAll, deleteById, count, paging and sorting.
Who implements it: Spring Data creates a proxy object for the interface at startup.
Where the work goes: standard calls go to a built-in implementation backed by JPA.
“Extending JpaRepository with the entity type and id type gives me a set of ready methods: save, findById, findAll, deleteById, count, existsById, plus paging and sorting. I never write the class because Spring Data writes it for me at runtime. At startup it finds every repository interface and creates a proxy object that implements it, then registers that proxy as a bean. When I call save, the proxy passes it to a built-in class, SimpleJpaRepository, which uses the JPA EntityManager underneath. For custom method names, the proxy has already worked out the query from the name when the app started, which is why a typo in a method name makes startup fail instead of failing later. So when I inject StudentRepository into a service, I'm really getting that generated proxy.”
Saying Spring generates Java source files, or having no idea and calling it magic.
All or nothing: every database change in the method commits together or none do.
Example: debit one account, credit another; a failure between them must undo the debit.
Where it goes: the service method, and the default rollback rule.
“@Transactional means all the database work inside the method happens as one unit. Take a transfer: I take money from account A and add it to account B. If the code fails after the debit but before the credit, without a transaction A has lost money and B got nothing. With @Transactional on the service method, Spring starts a transaction before the method runs and commits when it returns normally. If a runtime exception escapes, it rolls everything back, so A's balance goes back too. I put it on the service layer, because that's where one business action might touch several tables. One thing I know to watch: by default it rolls back on unchecked exceptions, not checked ones, unless you say so with rollbackFor.”
Thinking it makes a method thread-safe, or that it has something to do with network requests.
Controller: HTTP only; reads the request, calls the service, returns the response.
Service: the business rules and transactions.
Repository: talks to the database; nothing else does.
“In my hostel booking project, the BookingController only dealt with HTTP. It read the room id and dates from the request and returned the right status. The BookingService held the rules: a student can't book two rooms for the same dates, and a room can't be over capacity. It was also where I put @Transactional. The BookingRepository only talked to the database. Keeping them apart helped in two ways. When we added a mobile screen that booked rooms in a slightly different flow, the rules were already in one place, so we didn't copy them. And testing the service was easy: I could mock the repository and check the rules without starting a server or a database. When it was all in the controller early on, one method was over a hundred lines.”
Putting business rules in the controller, or saying layers exist only because tutorials use them.
Hide fields: entities often hold data the client must not see, like a password hash.
Avoid JSON trouble: lazy relations and two-way links break or loop during serialization.
Freedom: the table can change without breaking the API.
“A DTO is a simple class, often a Java record, shaped for what the API should send. The first reason is safety. My User entity had a password hash field, and returning the entity would have sent it to the browser. The second reason is that entities cause JSON trouble. In my project, Student had a list of Courses and Course pointed back to Students. When I returned the entity, Jackson went back and forth between them until it failed with a stack overflow. Lazy relations can also throw errors when they're read outside a transaction. The third is freedom: if I rename a column or split a table, the DTO keeps the API the same for the frontend. It costs a bit of mapping code, but that's worth it.”
Saying DTOs are just extra boilerplate with no purpose.
Never plain text: store a one-way hash, not the password or an encrypted version.
BCrypt: slow on purpose, with a random salt built in; Spring gives you BCryptPasswordEncoder.
Checking: matches compares the typed password against the stored hash.
“I'd never store the password itself. I'd store a hash, which is a one-way scramble that can't be turned back into the password. Spring Security has a PasswordEncoder interface, and I'd use BCryptPasswordEncoder. On sign-up, I call encode on the password and save the result. BCrypt adds a random salt each time, so two users with the same password get different hashes, and it's deliberately slow, which makes guessing millions of passwords expensive. On login I don't hash and compare strings myself. I call matches with the typed password and the stored hash, and it handles the salt. In my first project I stored plain text and only fixed it when a senior pointed it out, so this is something I now do from the first day.”
Saying you'd encrypt the password so you can decrypt it later, or storing plain text.
Mock the repository: Mockito gives a fake whose answers you control.
Real service: create it with the mock passed into its constructor.
Check the result: assert the happy path and the error path.
“Because my service takes its repository through the constructor, I don't need Spring at all. With JUnit 5 and Mockito, I mark the repository with @Mock and the service with @InjectMocks, and Mockito builds the service with the fake repository inside. Then I tell the mock what to return, for example that findById for id 7 returns an empty Optional. I call the service method and check it throws my StudentNotFoundException. A second test returns a real student and checks the DTO has the right name. These tests run in milliseconds, since no application context or database starts. I'd save the slower @SpringBootTest style for checking that the pieces work together.”
@ExtendWith(MockitoExtension.class)
class StudentServiceTest {
@Mock StudentRepository repository;
@InjectMocks StudentService service;
@Test
void throwsWhenStudentMissing() {
when(repository.findById(7L)).thenReturn(Optional.empty());
assertThrows(StudentNotFoundException.class, () -> service.get(7L));
}
}
Saying every test needs @SpringBootTest, or never having written a test.
What and why: the problem in one line, and the main parts of the app.
Your part: the endpoints, tables or features you wrote yourself.
Looking back: one honest thing you would change and why.
“In my final year, three of us built a canteen pre-order app. Students ordered from their phones and the canteen saw orders on a screen. The backend was Spring Boot with MySQL, and the frontend was React. I owned the backend: the entities for menu items, orders and order lines, the REST endpoints, and the rule that an order closes fifteen minutes before pickup. I also wrote the login using Spring Security and BCrypt. What I'd do differently is that I returned entities straight from the controllers, and we hit an infinite JSON loop between Order and OrderLine. I patched it with an annotation. Now I'd use DTOs from the start. I'd also write tests earlier, because we found most bugs during the demo week.”
Describing the team's work as all yours, or being unable to explain code you listed on your resume.
The error: what you saw, in one or two lines.
The method: read the full stack trace, form a guess, test it small.
The lesson: what you do differently since.
“In my final-year project, my POST endpoint for adding a student returned 200, but every row in the table had a null name and email. There was no error at all, which made it confusing. I first blamed the database mapping and spent a while changing column annotations, which was a waste. Then I stopped guessing and put a breakpoint on the first line of the controller method. The DTO was already empty there, so the database was never the problem. That narrowed it to how the request was read. I'd forgotten @RequestBody on the parameter, so Spring tried to fill the object from query parameters and simply ignored the JSON body. I added it and it worked. What I took from it is to find where the data goes wrong before changing anything, and I now test new endpoints with a real request straight away.”
A story where the fix was pasted from a forum without knowing why it worked.
Treat it as leaked: change the password first, because git history keeps the old one.
Fix the setup: read secrets from environment variables and keep local files out of git.
Handle the person: tell them privately and fix the process, not the blame.
“First I'd treat the password as already leaked. Even if we delete the line in the next commit, it stays in the git history, and public repos get scanned by bots quickly. So the first step is to change the database password. Then I'd fix the setup so it can't happen again. The committed properties file only has safe local values, and the real password comes from an environment variable, which Spring Boot reads at startup. I'd add a local properties file to .gitignore for anyone who prefers a file. I'd message the teammate privately and keep it light, because I've nearly done the same thing. Then I'd suggest the whole group check the repo for other keys, like an email or cloud key.”
Only deleting the line and moving on, or calling out the teammate in the group chat.
ClapAssist is an AI interview assistant for Mac and Windows. It listens to the interview on your computer and shows you what to say, in short lines you can read while you talk. Your live interview audio and screen are never stored. Your resume and notes are saved to your account so the app fills them in on any computer. It stays out of screen share on every plan, including Free; only you can see it.