GetYourGuide Senior software Engineer Java Interview Guide — Berlin

Recently I had the opportunity to attend senior software engineer — Java position for GetyourGuide in Berlin. I wanted to share my experience of the interview and provide useful tips as to what was asked in the interview and what do they expect from the candidates etc.

Get Your Guide : Senior software engineer (Java) Interview Process
Pic credits : Get Your Guide -Senior software engineer (Java) Interview Process

The position which I applied can be seen in their careers page.

Interview Round:
1. HR Interview
2. HackerRank Test
3. Live coding challenge
4. System design
5. Culture Fit

To know more about the interview process , refer their page.

The very first round was HR interview where my past experience was asked , company history and about the next steps. Show interest in the company and the technical skills they are looking for in a candidate.

The second was the Hacker rack test where HackerRank Event Registration System had to be implemented. The problem statement and solution is defined here.

The third round was the live coding round where we need to refactor certain section of the project , identify issues and think out loud, explain your appproach . There will be 2 members from the team joining this meeting , one would be around your experience and the other would be a Technical Manager. They would assess your approach , how quick you are able to understand the problem and your solution.

They will share the Github codebase 1 day before the interview. You can also find the same codebase in their Github Page.

In this interview ,I was asked to refactor some code , write unit tests , how i can write the entire new endpoint etc.

I have noted down some of the issues they can ask here. If you go through questions , Im sure its more than enough to clear your coding interviews and focus on system design round followed by culture fit.

There was issues introduced at each level i.e from entity , service, controller and exception handler etc. I will try to summarise few of those issues here. This is how an end to end application should be given to develop for any coding interviews.

To know the complete list of questions they might ask, refer to my Github cheat-sheet.

Entity Issues:

JPA entities look simple but have a lot of subtle traps. Here’s what was wrong:

Activity.java
1.
@EqualsAndHashCode on entity with all fields

Lombok’s @EqualsAndHashCode generates methods that read all fields. On a JPA entity, this triggers lazy-loaded relationships — causing N+1 queries or LazyInitializationException outside a transaction. It also breaks Set<Activity> collections because hash codes change when the entity is saved (ID changes from 0 to a real value).

2. @ToString without @ToString.Exclude on relationships

// Dangerous — follows Supplier → Activity → Supplier → ...
@ManyToOne
private Supplier supplier;

// Use the below
// Safe
@ToString.Exclude
@ManyToOne
private Supplier supplier;

3. GenerationType.AUTO
AUTO lets the JPA provider choose the strategy — produces different behaviour on different databases. Use IDENTITY or SEQUENCE explicitly.

4. Primitive types instead of wrapper types

private int price;           // can't be null — Hibernate error or silent 0
private double rating; // can't be null
private boolean specialOffer; // defaults to false if DB has null
// Use wrapper types
private Integer price;
private Double rating;
private Boolean specialOffer;

5. Missing @JoinColumn
Without @JoinColumn(name = “supplier_id”), Hibernate auto-generates the column name. No control over nullability, harder to understand the schema.

Supplier.java

// Without @JsonIgnore — StackOverflowError at runtime
@OneToMany(mappedBy = "supplier", fetch = FetchType.LAZY)
private List<Activity> activities;
// Fix
@JsonIgnore
@OneToMany(mappedBy = "supplier", fetch = FetchType.LAZY)
private List<Activity> activities = new ArrayList<>();

Without @JsonIgnore, Jackson serializes Supplier, follows activities, serializes each Activity, follows supplier back — infinite recursion. Also: initialize the list to avoid NullPointerException when iterating.

Service Layer Issues:

The original ActivityService had the most bugs of any single file :

Bug 1 — Full Table Scan to Fetch a Single Activity
getActivities(Long activityId) loaded every activity from the DB, then filtered in Java:

public ActivityDto getActivities(Long activityId) {
List<Activity> activities = activityRepository.findAll(); // loads ALL rows
List<ActivityDto> result = new ArrayList<>();
activities.stream()
.filter(activity -> activityId.equals(activity.getId())) // filter in Java
.forEach(activity -> { result.add(...); });
return result.get(0); // IndexOutOfBoundsException if ID not found
}

The fix: activityRepository.findById(activityId).orElseThrow(() -> new ActivityNotFoundException(…)) — one DB call, no Java filtering, correct 404 behaviour.

Bug 2 — NullPointerException on Supplier (Inconsistent Null Handling)

The same class handled the same null case two different ways — one correct, one crashing:

// getActivities() — line 33 — correctly handles null supplier
.supplierName(Objects.isNull(activity.getSupplier()) ? "" : activity.getSupplier().getName())
// getActivities(Long) - line 50 - SAME CLASS, crashes with NPE if supplier is null
.supplierName(activity.getSupplier().getName()) // NullPointerException

Two methods in the same class, same field, different null-safety which has inconsistent code.

Bug 3 — Dead Code in searchActivities

public List<ActivityDto> searchActivities(String search) {
List<Activity> activities = activityRepository.findAll();
// DEAD CODE - built, allocated, never used
List<ActivityDto> activitiesDto = activities.stream().map(activity -> {
return ActivityDto.builder()...supplierName(getSupplierName(activity)).build();
}).toList(); // this entire block is thrown away immediately
// The actual result built again from scratch below
List<ActivityDto> result = new ArrayList<>();
activities.stream()
.filter(a -> a.getTitle().contains(search)) // case-sensitive
.forEach(activity -> { result.add(...build()); });
return result;
}

The DB is hit once, all activities are transformed to DTOs (immediately discarded), then all activities are iterated again. The dead activitiesDto variable allocates memory and CPU for nothing. Also: .contains(search) is case-sensitive — ”berlin” won’t match “Berlin City Tour”.

Bug 4 — Mutating External List Inside Stream

// Anti-pattern — mutating external state inside forEach
List<ActivityDto> result = new ArrayList<>();
activities.stream().forEach(activity -> {
result.add(...); // side-effecting a list from inside a stream
});
// Correct - streams are for transformation, not mutation
return activities.stream()
.map(activity -> ActivityDto.builder()...build())
.toList();

Bug 5 — Missing @Transactional(readOnly = true)

// Wrong — default transaction, full dirty checking overhead
public List<ActivityDto> getActivities() { ... }
// Correct - Hibernate skips dirty checking for read-only queries
@Transactional(readOnly = true)
public List<ActivityDto> getActivities() { ... }

Bug 6— Business Logic Leaking into the Service

// Two styles of null-check in the same class — neither belongs here
Objects.isNull(activity.getSupplier()) ? "" : activity.getSupplier().getName()
activity.getSupplier().getName() // no null check at all
// Correct - put it on the entity, use it consistently everywhere
public String getSupplierName() {
return Optional.ofNullable(supplier).map(Supplier::getName).orElse("");
}
.supplierName(activity.getSupplierName())

Bug 7 : Repeated Conversion Logic

activities.stream().forEach(activity -> {
result.add(ActivityDto.builder()
.id(activity.getId())
.title(activity.getTitle())
.price(activity.getPrice())
.currency(activity.getCurrency())
.rating(activity.getRating())
.specialOffer(activity.isSpecialOffer())
.supplierName(Objects.isNull(activity.getSupplier()) ? "" : activity.getSupplier().getName())
.build());
});
//ActivityService.java
public List<ActivityDto> getActivities() {
List<Activity> activities = activityRepository.findAll();
return activities.stream().map(this::toActivityToDto).toList();

}
private ActivityDto toActivityToDto(Activity activity) {
return ActivityDto.builder()
.id(activity.getId())
.title(activity.getTitle())
.price(activity.getPrice())
.currency(activity.getCurrency())
.rating(activity.getRating())
.specialOffer(activity.isSpecialOffer())
.supplierName(activity.getSupplierName())
.build();

}
===
//Activity.java
public String getSupplierName() {
return Optional.ofNullable(supplier).map(Supplier::getName).orElse("");
}

Bug 8 : There was no SupplierService class, the SupplierController was calling directly JPA queries.
We need to write SupplierService and SupplierRepository for the endpoints defined in SupplierController.

Bug 9 — N+1 Query Problem (Lazy Loading the Supplier)

Even after adding @Transactional(readOnly = true), getActivities() still has a hidden performance problem. Consider this:

// Query 1 — loads all activities:
activityRepository.findAll()
.stream()
.map(this::toActivityDto) // For EACH activity...
.collect(Collectors.toList());
private ActivityDto toActivityDto(Activity activity) {
return ActivityDto.builder()
.supplierName(activity.getSupplier().getName()) // Query N: SELECT * FROM supplier WHERE id = ?
.build();
}

Because supplier is FetchType.LAZY, Hibernate does not load it upfront. The first call to activity.getSupplier() triggers a separate SELECT to load that one supplier. For 100 activities this generates 101 queries. For 1000 activities, 1001 queries. This is the classic N+1 problem.

Fix — use LEFT JOIN FETCH in the repository to load everything in one query:

// In ActivityRepository:
@Query("""
SELECT a FROM Activity a
LEFT JOIN FETCH a.supplier
""")
List<Activity> findAllWithSuppliers();
// In ActivityService:
@Transactional(readOnly = true)
public List<ActivityDto> getActivities() {
return activityRepository.findAllWithSuppliers() // Always 1 query
.stream()
.map(this::toActivityDto)
.collect(Collectors.toList());
}

LEFT JOIN FETCH generates a single SQL join: SELECT a.*, s.* FROM activity a LEFT JOIN supplier s ON a.supplier_id = s.id. The supplier is already loaded — no extra queries per activity

Controller Issues:

1.SupplierController — The EntityManager Disaster

The original controller had this:

@PersistenceContext
private EntityManager entityManager; // belongs in repository
@GetMapping("/suppliers/search/{search}")
public ResponseEntity<List<Supplier>> suppliersSearch(@PathVariable String search) {
var list = entityManager.createNativeQuery(
"SELECT * FROM GETYOURGUIDE.SUPPLIER", Supplier.class) // hardcoded schema
.getResultList(); // full table scan
for (Supplier s : list) {
if (...contains(search)) {
return ResponseEntity.ok(List.of(s)); // first match only
}
}
return ResponseEntity.ok(list); // returns ALL when nothing matches
}

The fix — delete all of it. Use a JPQL query in the repository:

@Query("""
select s from Supplier s where
lower(s.name) like lower(concat('%', :search, '%')) OR
lower(s.city) like lower(concat('%', :search, '%')) OR
lower(s.country) like lower(concat('%', :search, '%'))
""")
List<Supplier> searchSupplier(@Param("search") String search);

2.Confusing Method Overloading in ActivitiesController

The original controller had two methods both named activities():

@GetMapping("/activities")
public ResponseEntity<List<ActivityDto>> activities() { ... } // get all
@GetMapping("/activities/{id}")
public ResponseEntity<ActivityDto> activities(@PathVariable Long id) { ... } // get one

Java allows this via method overloading, but it’s confusing — especially in a codebase someone else has to read. Also: no guard against id <= 0. Passing /activities/0 or /activities/-1 would hit the DB with an invalid ID and return a cryptic IndexOutOfBoundsException. Method names should be getAllActivities() and getActivityById(), and the ID should be validated before use.

3.@Controller vs @RestController

Both SupplierController and StatisticsController used @Controller. For a REST API returning JSON, this is wrong. Without @ResponseBody or @RestController, Spring tries to resolve a view and returns 404/500.

@Controller      // for MVC views only
@RestController // for REST JSON APIs

Repository Issues:

StatisticsRepository — Not Actually Statistics

The StatisticsRepository query in the original code was:

// This is NOT statistics — it's just SELECT * on the supplier table
String SUPPLIER_STATS_QUERY = """
SELECT s.* FROM getyourguide.supplier s
""";
@Query(value = SUPPLIER_STATS_QUERY, nativeQuery = true)
List<Object[]> getSupplierStats(); // raw arrays exposed in API response

Three problems:

  • The query is literally SELECT * on suppliers — no aggregation. A stats endpoint should return COUNT, SUM, AVG of activities per supplier.
  • Returns List<Object[]> — raw DB arrays with no field names leaked straight to the client: [[1, “John Doe”, …], …].
  • Hardcoded schema getyourguide in a native query — breaks on any other database or schema rename.

Exception Handling:

1.Current Issue: Minimal exception handling, basic error responses

Expected Solution:

@ControllerAdvice
@Slf4j
public class ErrorHandler {

@ExceptionHandler
public ResponseEntity<ErrorResponse> handleBadRequest(IllegalArgumentException exp) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse("Bad Request", exp.getMessage(), System.currentTimeMillis()));
}

@ExceptionHandler
public ResponseEntity<ErrorResponse> handleException(ActivityNotFoundException exp) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse("Activity Not Found", exp.getMessage(), System.currentTimeMillis()));
}

@ExceptionHandler
public ResponseEntity<ErrorResponse> handleException(Exception exp) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("Server Error", exp.getMessage(), System.currentTimeMillis()));
}


}

The Complete Testing Strategy

Once everything was fixed, I implemented a full test covering every layer.

[ @SpringBootTest + @AutoConfigureMockMvc ] ← Full stack integration
[ @WebMvcTest + @MockitoBean ] ← Web layer unit tests
[ Plain JUnit + Mockito.mock() ] ← Service unit tests
[ @DataJpaTest + TestEntityManager ] ← JPA slice tests

Layer 1 — @DataJpaTest for Repositories

@DataJpaTest(properties = {
"spring.sql.init.mode=never",
"spring.jpa.hibernate.ddl-auto=create"
})
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class ActivityRepositoryTest {
@Autowired
private ActivityRepository activityRepository;
@Autowired
private TestEntityManager entityManager;
}

@DataJpaTest loads only the JPA slice — no web layer, no services. Fast and isolated. The replace = NONE is important without it, Spring Boot replaces your configured datasource with its own, which can conflict with custom properties.

TestEntityManager.flush() + clear() after setup forces the data to hit the DB and clears the first-level cache, so findBy* queries hit real SQL — not the Hibernate session cache.

Layer 2 — Plain JUnit + Mockito for Services

class SupplierServiceTest {
private SupplierRepository supplierRepository;
private SupplierService supplierService;
@BeforeEach
void setup() {
supplierRepository = mock(SupplierRepository.class);
supplierService = new SupplierService(supplierRepository, ...);
}
}

No Spring context at all — pure unit tests with mock(). Constructor injection makes this trivial. Services that use field injection (@Autowired) are much harder to unit test cleanly.

Layer 3 — @WebMvcTest for Controllers

@WebMvcTest(ActivityController.class)
public class ActivitiesControllerTest {
@Autowired
private MockMvc mockMvc;
@MockitoBean // ← Spring Boot 4.x - @MockBean was removed
private ActivityService activityService;
}

The @ControllerAdvice (ErrorHandler) is automatically included in @WebMvcTest — so you can assert that invalid IDs return 400 and not-found cases return 404, all without a real database.

Layer 4 — @SpringBootTest for Integration Tests

@SpringBootTest(properties = {
"spring.sql.init.mode=never",
"spring.jpa.hibernate.ddl-auto=create-drop"
})
@AutoConfigureMockMvc
public class ActivityControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ActivityRepository activityRepository;
}

Full Spring context, real H2, real service, real repository. MockMvc still sends HTTP requests through the controller. This catches wiring bugs that @WebMvcTest misses — for example, a JPQL query that compiles but returns wrong results.

Performance Optimization

Issue 1: N+1 Query Problem

Location: ActivityService.getActivities()

@Transactional(readOnly = true)
public List<ActivityDto> getActivities() {
return activityRepository.findAll().stream() // Query 1: SELECT * FROM activity
.map(this::toActivityDto) // For each activity:
.collect(Collectors.toList());
}
private ActivityDto toActivityDto(Activity activity) {
return ActivityDto.builder()
.supplierName(activity.getSupplierName().orElse("")) // Query N: SELECT * FROM supplier WHERE id = ?
.build();
}

What They’ll Ask:

  • “How many queries does this generate for 100 activities?”
  • “What is the N+1 problem?”
  • “How do you fix it?”

Expected Solution:

// SupplierRepository:
@Query("""
SELECT a FROM Activity a
LEFT JOIN FETCH a.supplier
""")
List<Activity> findAllWithSuppliers();
// SERVICE:
@Transactional(readOnly = true)
public List<ActivityDto> getActivities() {
return activityRepository.findAllWithSuppliers()
.stream()
.map(this::toActivityDto)
.collect(Collectors.toList());
}

Feedback :

This was the feedback I had received:

Pros:
Clear Communication: The candidate gave a short and clear introduction, and communicated their thought process during the debugging task.
Log Analysis: Able to use logs effectively to identify the problematic code line.
Willingness to Test: Mentioned the importance of testing and attempted to write test cases after the fix.
Curiosity: Asked relevant questions about the team and tech stack, showing interest in the broader context.
Eventual Resolution: With guidance, was able to arrive at the correct solution for the bug.

Cons:
Missed Root Cause: Initially missed the null supplier issue and focused on the wrong null check.
Incomplete Testing: Attempted but was unable to fully implement the test cases for their fix.
Time Management: Took significantly longer than expected (10 minutes) for the debugging task, leaving little time for the other task.
Limited Refactoring Input: Due to time constraints, provided only a high-level suggestion (move logic to repository) for the refactoring task, without detailed implementation.

Though I couldnt progress to the system design round but you can also find what possible questions they might ask in these rounds in Glassdoor. Some of the system design questions would be “design ticket service”

Overall the whole interview process was good and the entire GetyourGuide employees were very friendly and supportive. I would definetely suggest people could try these companies since the learning curve is more in such companies.
The one thing you need to focus is on your Core Java and SpringBoot skills atleast for coding rounds. For the system design be prepared with all the basic system design concepts and also have understanding of how GetYourGuide at system level.

The culture fit is also equally important. It shows how you can blend with the team if they hire you.
Show interest in the company and the role you are applying for.

Good luck with your interviews! Feel free to ask questions in the comments. You can find the complete working code on my GitHub Page.

Kindly note I have solved this code using SpringBoot4.0.3 ,Java 17 and Maven but their repository has Gradle and Spring3.5.7 , and Java 21 version although the solution is same.

Thank you for reading this article. Please provide your valuable suggestions/ feedback.

  • Clap and Share if you liked the content.
  • 📰 Read more content on my Medium (on Java Developer interview questions)
  • 🔔 Follow me on: LinkedIn
  • Reach out to me for resume and interview preparation . Contact me on topmate.

Please find my other helpful articles on Java Developer interview questions.

Following are some of the frequently asked Java 8 Interview Questions

Frequently Asked Java Programs

Dear Readers, these are the commonly asked Java programs to check your ability in writing the logic.

SpringBoot Interview Questions | Medium

Rest and Microservices Interview Questions| Medium

Spring Boot tutorial | Medium

Must-know-coding-programs-for-faang-companies| Medium


GetYourGuide Senior software Engineer Java Interview Guide — Berlin was originally published in Javarevisited on Medium, where people are continuing the conversation by highlighting and responding to this story.

This post first appeared on Read More