Spring Boot Pre-Interview Guide Comprehensive Assignment: Marketplace REST API — Spring Boot 4 · Kotlin 2.3

Spring Boot Pre-Interview Guide Comprehensive Assignment: Marketplace REST API — Spring Boot 4 · Kotlin 2.3


Introduction

“How far should I take a Spring Boot pre-interview assignment in seven days?”

Parts 1 through 7 walked through the areas reviewers actually look at, one topic per post. Real assignments do not arrive divided that way. A single REST API implementation mixes four-layer responsibilities, JPA mapping, testing, N+1 hotspots, JWT auth, Docker deployment, and event-driven notifications into one codebase. So the series closes with one comprehensive assignment.

This post is the series capstone and a spec sheet. The brief is to build a fictional online marketplace REST API in seven days, covering member, product, and order domains. Each section flags which earlier part applies. Approach it as if you were simulating how a reviewer reads your README and code, in that order.

The target reader is a junior backend engineer who has skimmed Parts 1–7 once. You do not need to implement every item — clearing the 70-point base and then picking your bonus battles is the realistic strategy. Even one clear paragraph in the README explaining “why this structure” widens the gap against other submissions.

Back to Part 7


TL;DR

  • A seven-day marketplace REST API — members (BUYER/SELLER/ADMIN), product CRUD, order create/cancel, image upload, search with pagination, caching, and async notifications. Every area Parts 1–7 covered lands in one project.
  • Stack is Spring Boot 4 + Kotlin 2.3 — Java 21 recommended, JPA/Hibernate, Gradle, H2 (local) + MySQL 8 (Docker), QueryDSL and Redis optional. No Lombok.
  • Structure choice is the first scoring signal — pick single-module (recommended) or multi-module (Option A strict Dependency Inversion Principle / Option B simplified) and apply it consistently. Not stating why in the README is itself a deduction.
  • 70 base + 35 bonus points — base covers features, code quality, design, tests. Bonus covers Docker, Swagger, CI, caching, events, QueryDSL, multi-module. Deductions hit harder: build failure -20, missing README -10, plain-text passwords -10.
  • Five checks before you submit./gradlew build passes, docker-compose up works, Swagger reachable, no secrets or .idea committed, README complete. Reviewers only start reading your real code once these five hold.

1. Assignment Overview

1.1 What you are building

Implement the backend API for an online marketplace. Sellers register products; buyers search and place orders. Notifications run asynchronously and popular products are cached — the same shape as a real production service.

flowchart TB
    subgraph Series["What Parts 1–7 covered"]
        P1["Part 1 — Core 4 layers"]
        P2["Part 2 — Database & Testing"]
        P3["Part 3 — Documentation & AOP"]
        P4["Part 4 — Performance · N+1"]
        P5["Part 5 — Security · JWT"]
        P6["Part 6 — DevOps · Docker"]
        P7["Part 7 — Events · multi-module"]
    end

    subgraph Assignment["Comprehensive assignment (this post)"]
        Auth["JWT auth"]
        Product["Product CRUD + search"]
        Order["Order create/cancel"]
        Cache["Popular-product caching"]
        Event["Order event notifications"]
        Deploy["Docker Compose deployment"]
    end

    P1 --> Product
    P1 --> Order
    P2 --> Product
    P3 --> Auth
    P4 --> Product
    P5 --> Auth
    P6 --> Deploy
    P7 --> Event
    P7 --> Cache

The diagram shows where each part’s patterns land. If you get stuck in one area while building, drop back to the corresponding part and re-read it.

Note — Spring Boot 4 + Kotlin 2.3 setup: This assignment assumes Spring Boot 4 + Kotlin 2.3. The Kotlin plugin setup (kotlin-spring, kotlin-jpa, kotlin("plugin.spring") version "2.3", etc.) is covered in Part 1 §1.1 — start there. The Kotlin 2.x line is backward compatible, so the same code runs on 2.0 through 2.3. Lombok is not used.

1.2 Deadline and tech stack

ItemDetail
Deadline7 days from the date the assignment is received
Language / frameworkKotlin 2.3, Spring Boot 4 (Java 21 recommended)
PersistenceJPA/Hibernate, Gradle
DatabaseH2 (local), MySQL 8.0 (Docker)
OptionalQueryDSL, Redis

2. Business Requirements

2.1 Member management

  • Member types: BUYER, SELLER, ADMIN
  • Email duplication check on signup
  • JWT issuance on login (Access Token + Refresh Token)
  • Business registration number is required for sellers

2.2 Product management (sellers only)

  • Product create/update/delete (own products only)
  • Product image upload (up to 5 images, max 10 MB each)
  • Product status: DRAFT, ON_SALE, SOLD_OUT, DELETED
  • Inventory management

2.3 Product browsing (public)

  • Product list (pagination, search, filtering)
  • Product detail
  • Category-based listing
  • Popular products list (cached)

2.4 Order management

  • Buyer: create order, cancel order, view order history
  • Seller: view orders for own products, update shipping status
  • Order status transitions: PENDINGCONFIRMEDSHIPPEDDELIVERED
  • Cancellation is only allowed in PENDING or CONFIRMED

2.5 Notifications

  • Notify the seller when an order is created (async)
  • Notify the buyer when shipping status changes (async)
  • Logging stands in for actual delivery (no real notification provider required)

3. API Specification

3.1 Authentication API

MethodURIDescriptionAuth
POST/api/v1/auth/signupSign upX
POST/api/v1/auth/loginLoginX
POST/api/v1/auth/refreshToken refreshX

3.2 Member API

MethodURIDescriptionAuth
GET/api/v1/members/meGet my infoO
PATCH/api/v1/members/meUpdate my infoO
GET/api/v1/admin/membersMember list (admin)ADMIN

3.3 Product API

MethodURIDescriptionAuth
POST/api/v1/productsRegister productSELLER
GET/api/v1/productsProduct listX
GET/api/v1/products/{productId}Product detailX
PATCH/api/v1/products/{productId}Update productSELLER (owner)
DELETE/api/v1/products/{productId}Delete productSELLER (owner)
POST/api/v1/products/{productId}/imagesUpload product imagesSELLER (owner)
GET/api/v1/products/popularPopular products listX

3.4 Order API

MethodURIDescriptionAuth
POST/api/v1/ordersCreate orderBUYER
GET/api/v1/ordersMy order listO
GET/api/v1/orders/{orderId}Order detailO (owner)
POST/api/v1/orders/{orderId}/cancelCancel orderBUYER (owner)
GET/api/v1/sellers/ordersSeller order listSELLER
PATCH/api/v1/sellers/orders/{orderId}/statusUpdate shipping statusSELLER

3.5 Category API

MethodURIDescriptionAuth
GET/api/v1/categoriesCategory listX
POST/api/v1/admin/categoriesRegister categoryADMIN

4. Detailed Requirements

4.1 Authentication and authorization

  • JWT-based auth — Access Token 1 hour, Refresh Token 7 days
  • Passwords encrypted with BCrypt (see Part 5 §3.1)
  • Role-based access control — BUYER, SELLER, ADMIN
  • Resource ownership verification — owners can modify only their own products/orders (Part 5 §4.3)

4.2 Product search and filtering

Example query:

GET /api/v1/products?keyword=laptop&categoryId=1&minPrice=100000&maxPrice=2000000&status=ON_SALE&page=0&size=20&sort=createdAt,desc
ParameterTypeDescription
keywordStringProduct name search (partial match)
categoryIdLongCategory filter
minPriceBigDecimalMinimum price
maxPriceBigDecimalMaximum price
statusStringProduct status
sellerIdLongSeller filter
pageIntegerPage number (starts at 0)
sizeIntegerPage size (default 20, max 100)
sortStringSort key (createdAt, price, salesCount)

4.3 Order creation

Request body:

// POST /api/v1/orders
{
  "orderItems": [
    { "productId": 1, "quantity": 2 },
    { "productId": 3, "quantity": 1 }
  ],
  "shippingAddress": {
    "zipCode": "12345",
    "address": "123 Teheran-ro, Gangnam-gu, Seoul",
    "addressDetail": "Unit 456",
    "receiverName": "John Doe",
    "receiverPhone": "010-1234-5678"
  }
}

Processing rules:

  • Verify stock then deduct (handle concurrency — pessimistic or optimistic-lock-with-retry, see the appendix hint)
  • Allow simultaneous orders across multiple sellers (orders split per seller)
  • Publish a notification event to the seller on order creation (Part 7 §1)
  • Fail the order if inventory is insufficient

4.4 File upload

  • Supported extensions: jpg, jpeg, png, gif
  • Max file size: 10 MB
  • Up to 5 images per product
  • Storage path: /uploads/products/{productId}/{filename}
  • Filenames are rewritten to UUIDs before saving (see Part 7 §3)

4.5 Caching

TargetTTLNote
Popular products list10 minSee Part 4 §4
Category list1 hourRare changes
Product detail (optional)5 minInvalidate on update

4.6 Logging

  • Attach a unique Request ID to every request via MDC (Part 3 §2)
  • Log API requests/responses via AOP (Part 3 §2)
  • Log format: [timestamp] [level] [requestId] [class] message

5. Technical Requirements

5.1 Project structure options

Pick single-module or multi-module and apply it consistently. The choice itself matters less than naming the reason in the README — that one paragraph is the difference between “uncertain” and “deliberate” in the reviewer’s notes.

marketplace/
└── src/main/kotlin/com/example/
    ├── controller/
    ├── service/
    ├── repository/
    ├── domain/
    ├── dto/
    └── config/

Option B: multi-module (challenge)

Two variants exist (see Part 7 §6).

B-1. Strict (DIP applied)

marketplace/
├── marketplace-api/           # Controller, Security, execution
├── marketplace-domain/        # Entity, Service, Repository interfaces
├── marketplace-infra/         # Repository implementations, external integrations
└── marketplace-common/        # Common exceptions, utilities

B-2. Simplified (pragmatic)

marketplace/
├── marketplace-api/           # Controller, Service, Security, execution
├── marketplace-domain/        # Entities only
├── marketplace-infra/         # JpaRepository, QueryDSL
└── marketplace-common/        # Common exceptions, utilities

Module dependency directions:

flowchart TB
    subgraph OptionA["Option A strict (DIP)"]
        A_API["marketplace-api"]
        A_Domain["marketplace-domain<br/>Repository interfaces"]
        A_Infra["marketplace-infra<br/>Repository impls"]
        A_Common["marketplace-common"]

        A_API --> A_Domain
        A_API --> A_Infra
        A_Infra -->|DIP| A_Domain
        A_Domain --> A_Common
        A_Infra --> A_Common
    end

    subgraph OptionB["Option B simplified"]
        B_API["marketplace-api<br/>Service included"]
        B_Domain["marketplace-domain<br/>Entities only"]
        B_Infra["marketplace-infra<br/>JpaRepository"]
        B_Common["marketplace-common"]

        B_API --> B_Domain
        B_API --> B_Infra
        B_Infra --> B_Domain
        B_API --> B_Common
        B_Infra --> B_Common
    end

Multi-module requirements:

  • Apply the chosen structure (B-1 or B-2) consistently
  • B-1: no domain → infra dependency; separate Repository interface and implementation
  • B-2: Services live in api; use JpaRepository directly
  • State the chosen structure and rationale in the README

5.2 Required implementation

ItemDescription
Layer separationController → Service → Repository, DTO/Command split (Part 1 §2–§4)
Exception handlingGlobalExceptionHandler, custom exceptions, consistent error response (Part 1 §5)
ValidationBean Validation on Request DTOs (Part 1 §2.2)
TransactionsService-layer transaction management, readOnly split (Part 1 §3.2)
TestingController, Service, Repository tests (at least one each, Part 2 §3–§5)
API documentationSwagger or REST Docs (Part 3 §1)
DockerDockerfile + docker-compose.yml (App + MySQL, Part 6 §1–§2)
READMEHow to run, tech stack rationale, link to API docs

5.3 Optional implementation (bonus)

ItemDescriptionReference
Multi-moduleapi/domain/infra/common split, DIP appliedPart 7 §6
QueryDSLDynamic search queriesPart 4 §5.2
Redis cachingPopular products cachingPart 4 §4.3
GitHub ActionsCI pipeline (build/test)Part 6 §3
Test coverageJaCoCo 70%+Part 6 §3.2
Event-drivenOrder/notification event separationPart 7 §1

6. Data Model

Six entities with the following relationships:

erDiagram
    Member ||--o{ Product : "sells (SELLER)"
    Member ||--o{ Order : "places (BUYER)"
    Product ||--o{ ProductImage : "has"
    Product ||--o{ OrderItem : "ordered as"
    Category ||--o{ Product : "categorizes"
    Category ||--o{ Category : "parent"
    Order ||--|{ OrderItem : "contains"

    Member {
        Long id PK
        String email UK
        String password
        String name
        String phone
        String role "BUYER SELLER ADMIN"
        String businessNumber "SELLER only"
    }
    Product {
        Long id PK
        Long sellerId FK
        Long categoryId FK
        String name
        BigDecimal price
        Integer stockQuantity
        String status "DRAFT ON_SALE SOLD_OUT DELETED"
        Long salesCount
    }
    ProductImage {
        Long id PK
        Long productId FK
        String imageUrl
        Integer displayOrder
    }
    Category {
        Long id PK
        String name
        Long parentId FK
        Integer displayOrder
    }
    Order {
        Long id PK
        Long buyerId FK
        String orderNumber UK
        String status "PENDING CONFIRMED SHIPPED DELIVERED CANCELLED"
        BigDecimal totalAmount
    }
    OrderItem {
        Long id PK
        Long orderId FK
        Long productId FK
        Long sellerId FK
        String productName "snapshot"
        BigDecimal productPrice "snapshot"
        Integer quantity
        BigDecimal subtotal
    }

Notice that OrderItem stores product name and price as snapshots — order history must survive later product edits with the price at the time of purchase intact.


7. Evaluation Criteria

7.1 Base score (70 points)

ItemPointsDetail
Feature implementation30Requirements met, works correctly
Code quality20Readability, naming, consistency
Design10Layer separation, responsibility, exception handling
Testing10Coverage and test quality

7.2 Bonus (35 points)

ItemPoints
Docker Compose runnable+5
Swagger / REST Docs+5
GitHub Actions CI+5
Caching (Redis or local)+5
Event-driven notifications+5
QueryDSL dynamic queries+5
Multi-module structure with DIP+5

7.3 Deductions

ItemDeduction
Build failure-20
Missing/sparse README-10
No tests-10
SQL Injection vulnerability-10
Plain-text password storage-10
N+1 problem (obvious cases)-5

The bonus tops out at +35, but a single deduction line can take -20 in one shot. Lock the base in first, then chase bonus where it costs you the least.


8. Submission Guide

8.1 How to submit

  1. Push the code to a GitHub repository
  2. Include the following in README.md:
    • How to run (local, Docker)
    • Tech stack and rationale for choices
    • How to reach API documentation
    • Project structure overview
    • Any additional implementation notes
  3. Submit the repository URL

8.2 Run instructions

Single module

# Local (H2)
./gradlew bootRun --args='--spring.profiles.active=local'

# Docker Compose
docker-compose up -d

Multi-module

# Local (H2)
./gradlew :marketplace-api:bootRun --args='--spring.profiles.active=local'

# Build the runnable JAR
./gradlew :marketplace-api:bootJar

# Docker Compose
docker-compose up -d

8.3 Seed test accounts

RoleEmailPassword
ADMINadmin@example.comadmin123!
SELLERseller@example.comseller123!
BUYERbuyer@example.combuyer123!

8.4 Checklist

Confirm each before submission:

  • ./gradlew build succeeds
  • docker-compose up runs successfully
  • Swagger UI or REST Docs is reachable
  • All tests pass
  • README.md is complete
  • Sensitive files excluded (.env, secret keys, etc.)
  • Junk files excluded (.idea, .DS_Store, etc.)

Questions: ask by email during the assignment. If a requirement is ambiguous, make a reasonable judgement, implement accordingly, and document the choice in the README.


Recap

  • Parts 1–7 meet in one codebase — four layers (Part 1), JPA and tests (Part 2), documentation and logging (Part 3), N+1 optimization (Part 4), JWT (Part 5), Docker (Part 6), events and multi-module (Part 7) all land in the same marketplace. Drop back to a part when you get stuck.
  • The README speaks for your structure choice — whether you picked single-module or multi-module, one paragraph stating why is itself a scoring item. Without it, reviewers start deducting before they even read the code.
  • The 70 base outweighs the 35 bonus — avoiding -20 for a broken build, -10 for a thin README, and -10 for plaintext passwords matters more than chasing every bonus tile. Build the foundation first.
  • Spring Boot 4 + Kotlin 2.3 is the default — no Lombok needed; primary constructors with val/var, data classes, scope functions, and when expressions cut the code naturally.
  • Seven days is short — use the appendix’s implementation-order hints to fix a daily checkpoint. Leave the final day entirely for README polish and build verification — that is what separates rushed submissions from clean ones.

This post closes the Spring Boot pre-interview guide series. From the first Controller line in Part 1 to the entire marketplace in this assignment, you have now seen every area reviewers check. If you followed the series end to end, you know where the next pre-interview brief will not stall you. Good luck!

👉 Implementation code


Appendix

Implementation order — single module
  1. Project setup: dependencies, profile split, Docker Compose
  2. Domain design: Entity, Repository
  3. Authentication: Spring Security, JWT (Part 5)
  4. Member API: signup, login, my info
  5. Product API: CRUD, image upload
  6. Order API: create, query, status changes
  7. Search/pagination: product search, filtering (Part 4)
  8. Caching/events: popular product caching, notification events (Part 7)
  9. Tests: unit + integration (Part 2)
  10. Documentation: Swagger setup, README
Implementation order — multi-module
  1. Project structure: settings.gradle.kts, per-module build.gradle.kts
  2. common module: shared exceptions, ErrorCode, utilities
  3. domain module: Entity, Repository interfaces, Services (Option A) / entities only (Option B)
  4. infra module: Repository implementations, JPA configuration
  5. api module: Controller, Security, Swagger
  6. Integration tests: end-to-end flow exercised from api
  7. Docker: multi-module Dockerfile
  8. Documentation: README with the module diagram

Watch out: avoid circular dependencies once modules are split.

Concurrency hint — inventory deduction

Two ways to handle concurrent stock deduction. Pessimistic locks are simple and safe but reduce throughput; optimistic locks scale better but require retry on conflict.

// 1. Pessimistic lock
interface ProductRepository : JpaRepository<Product, Long> {
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("SELECT p FROM Product p WHERE p.id = :id")
    fun findByIdWithLock(@Param("id") id: Long): Product?
}

// 2. Optimistic lock + retry
@Entity
class Product(
    @Id @GeneratedValue
    val id: Long? = null,

    @Version
    var version: Long = 0,
    // ...
)
Event hint — notify after order creation

Use @TransactionalEventListener(phase = AFTER_COMMIT) so notifications fire only once the order transaction commits — the same pattern as Part 7 §1.

data class OrderCreatedEvent(
    val orderId: Long,
    val sellerId: Long,
    val createdAt: LocalDateTime = LocalDateTime.now(),
)

@Component
class OrderEventListener(
    private val notificationService: NotificationService,
) {
    @Async
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    fun handleOrderCreated(event: OrderCreatedEvent) {
        notificationService.notifySeller(event.sellerId, event.orderId)
    }
}
Multi-module hint — Gradle setup with Option A/B code

Two approaches for multi-module:

OptionService locationRepository handlingTrait
Option A (strict)domainInterface/impl splitStrict DIP
Option B (simplified)apiJpaRepository directPragmatic, less code

settings.gradle.kts

rootProject.name = "marketplace"

include("marketplace-api")
include("marketplace-domain")
include("marketplace-infra")
include("marketplace-common")

dependencyResolutionManagement {
    versionCatalogs {
        create("libs") {
            // Spring Boot 4 + Kotlin 2.3 BOM
        }
    }
}

Per-module dependencies

// marketplace-common: no dependencies (utilities + shared exceptions)

// marketplace-domain
dependencies {
    implementation(project(":marketplace-common"))
    implementation(libs.spring.boot.starter.data.jpa)
}

// marketplace-infra
dependencies {
    implementation(project(":marketplace-common"))
    implementation(project(":marketplace-domain"))
    implementation(libs.spring.boot.starter.data.jpa)
    // QueryDSL (optional)
    implementation("com.querydsl:querydsl-jpa:5.0.0:jakarta")
    kapt("com.querydsl:querydsl-apt:5.0.0:jakarta")
    runtimeOnly(libs.h2)
    runtimeOnly(libs.mysql.connector.j)
}

// marketplace-api (execution module)
dependencies {
    implementation(project(":marketplace-common"))
    implementation(project(":marketplace-domain"))
    implementation(project(":marketplace-infra"))
    implementation(libs.spring.boot.starter.web)
    implementation(libs.spring.boot.starter.security)
}

Option A: Repository interface / impl split (DIP)

// marketplace-domain/.../ProductRepository.kt (interface)
interface ProductRepository {
    fun save(product: Product): Product
    fun findById(id: Long): Product?
}

// marketplace-infra/.../ProductRepositoryImpl.kt (implementation)
@Repository
class ProductRepositoryImpl(
    private val jpaRepository: ProductJpaRepository,
) : ProductRepository {

    override fun save(product: Product): Product = jpaRepository.save(product)

    override fun findById(id: Long): Product? = jpaRepository.findById(id).orElse(null)
}

Option B: QueryDSL Custom Repository pattern (simplified)

// marketplace-infra/.../ProductJpaRepository.kt
interface ProductJpaRepository : JpaRepository<Product, Long>, ProductJpaRepositoryCustom {
    fun findBySellerId(sellerId: Long, pageable: Pageable): Page<Product>
}

// marketplace-infra/.../ProductJpaRepositoryCustom.kt
interface ProductJpaRepositoryCustom {
    fun search(keyword: String?, categoryId: Long?, pageable: Pageable): Page<Product>
}

// marketplace-infra/.../ProductJpaRepositoryImpl.kt (QueryDSL)
class ProductJpaRepositoryImpl(
    private val queryFactory: JPAQueryFactory,
) : ProductJpaRepositoryCustom {
    override fun search(keyword: String?, categoryId: Long?, pageable: Pageable): Page<Product> {
        // queryFactory.selectFrom(product).where(...).fetch()
        TODO()
    }
}

// marketplace-api/.../ProductService.kt (Service lives in the api module)
@Service
class ProductService(
    private val productJpaRepository: ProductJpaRepository,
) {
    // injected directly
}

Component scan config

// marketplace-api: MarketplaceApplication.kt
@SpringBootApplication(scanBasePackages = ["com.example"])
class MarketplaceApplication

fun main(args: Array<String>) {
    runApplication<MarketplaceApplication>(*args)
}
Multi-module Docker build hint

Java 21 + Gradle 8.10 multi-stage build, identical to the pattern in Part 6 §1.2.

FROM gradle:8.10-jdk21 AS builder
WORKDIR /app

# Copy Gradle files first (caching)
COPY build.gradle.kts settings.gradle.kts ./
COPY gradle ./gradle
COPY marketplace-common/build.gradle.kts ./marketplace-common/
COPY marketplace-domain/build.gradle.kts ./marketplace-domain/
COPY marketplace-infra/build.gradle.kts ./marketplace-infra/
COPY marketplace-api/build.gradle.kts ./marketplace-api/

RUN gradle dependencies --no-daemon || true

# Copy source and build
COPY . .
RUN gradle :marketplace-api:bootJar --no-daemon -x test

# Runtime
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=builder /app/marketplace-api/build/libs/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

External references

Shop on Amazon

As an Amazon Associate, I earn from qualifying purchases.