Spring Boot Pre-Interview Guide: Comprehensive Assignment

Spring Boot Pre-Interview Guide: Comprehensive Assignment


Series Navigation

PreviousCurrent
Part 7: Advanced PatternsComprehensive Assignment

Full Roadmap: See Spring Boot Pre-Interview Guide Roadmap

This assignment is a hands-on exercise that comprehensively applies everything covered in Parts 1 through 7.


Assignment Overview

Implement the backend API for an online marketplace. Sellers can register products, and buyers can search for products and place orders.

Submission Deadline

  • Deadline: 7 days from the date the assignment is received

Tech Stack

  • Required: Java 17+ or Kotlin, Spring Boot 3.x, JPA/Hibernate, Gradle
  • Database: H2 (local), MySQL 8.0 (Docker)
  • Optional: QueryDSL, Redis

Business Requirements

1. Member Management

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

2. Product Management (Seller Only)

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

3. Product Search (Public)

  • Product list retrieval (pagination, search, filtering)
  • Product detail retrieval
  • Category-based retrieval
  • Popular products list (with caching)

4. Order Management

  • Buyer: Create order, cancel order, view order history
  • Seller: View orders for own products, update shipping status
  • Order status: PENDING -> CONFIRMED -> SHIPPED -> DELIVERED
  • Order cancellation is only allowed in PENDING or CONFIRMED status

5. Notifications

  • Notify seller when an order is created (asynchronous)
  • Notify buyer when shipping status changes (asynchronous)
  • Notifications can be replaced with logs (actual delivery implementation not required)

API Specification

Authentication API

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

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

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

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

Category API

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

Detailed Requirements

1. Authentication/Authorization

[Requirements]
- JWT-based authentication (Access Token: 1 hour, Refresh Token: 7 days)
- Passwords encrypted with BCrypt
- Role-based access control (BUYER, SELLER, ADMIN)
- Resource owner verification (can only modify own products/orders)

2. Product Search/Filtering

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 from 0)
sizeIntegerPage size (default 20, max 100)
sortStringSort (createdAt, price, salesCount)

3. Order Creation

// 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"
  }
}
[Order Processing Rules]
- Check and deduct inventory (consider concurrency)
- Allow simultaneous ordering of products from multiple sellers (separate orders per seller)
- Publish notification event to seller upon order creation
- Fail the order if inventory is insufficient

4. File Upload

[Requirements]
- Supported extensions: jpg, jpeg, png, gif
- Max file size: 10MB
- Max 5 images per product
- Storage path: /uploads/products/{productId}/{filename}
- Filenames are converted to UUID before saving

5. Caching

[Caching Targets]
- Popular products list: 10-minute TTL
- Category list: 1-hour TTL
- Product detail (optional): 5-minute TTL, invalidated on update

6. Logging

[Requirements]
- Assign a unique Request ID to every request (MDC)
- API request/response logging (AOP)
- Log format: [timestamp] [level] [requestId] [class] message

Technical Requirements

Project Structure Options

Choose one of the following two structures for implementation.

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

Option B: Multi-Module (Challenge)

Two structure choices available:

B-1. Standard (with DIP)

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

Requirements when choosing multi-module:

  • Consistently apply the chosen structure (B-1 or B-2)
  • If B-1: No domain -> infra dependency, separate Repository interface/implementation
  • If B-2: Services are located in the api module, use JpaRepository directly
  • Specify the chosen structure and reasoning in README

Required Implementation

ItemDescription
Layer SeparationController -> Service -> Repository, DTO/Command separation
Exception HandlingGlobalExceptionHandler, Custom exceptions, Consistent error responses
ValidationBean Validation applied to Request DTOs
TransactionsService layer transaction management, readOnly separation
TestingController, Service, Repository tests (at least 1 each)
API DocumentationSwagger or REST Docs
DockerDockerfile + docker-compose.yml (App + MySQL)
READMEHow to run, Tech stack rationale, API documentation link

Optional Implementation (Bonus Points)

ItemDescription
Multi-Moduleapi/domain/infra/common separation, Dependency Inversion applied
QueryDSLDynamic search queries
Redis CachingPopular products caching
GitHub ActionsCI pipeline (build, test)
Test CoverageJaCoCo 70% or higher
Event-DrivenOrder/notification event separation
KotlinImplementation in Kotlin

Data Model (Reference)

Member
├── id (PK)
├── email (UNIQUE)
├── password (encrypted)
├── name
├── phone
├── role (BUYER, SELLER, ADMIN)
├── businessNumber (SELLER only)
├── createdAt
└── updatedAt

Product
├── id (PK)
├── sellerId (FK -> Member)
├── categoryId (FK -> Category)
├── name
├── description
├── price
├── stockQuantity
├── status (DRAFT, ON_SALE, SOLD_OUT, DELETED)
├── salesCount
├── createdAt
└── updatedAt

ProductImage
├── id (PK)
├── productId (FK -> Product)
├── imageUrl
├── displayOrder
└── createdAt

Category
├── id (PK)
├── name
├── parentId (FK -> Category, nullable)
└── displayOrder

Order
├── id (PK)
├── buyerId (FK -> Member)
├── orderNumber (UNIQUE)
├── status (PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED)
├── totalAmount
├── shippingAddress (embedded)
├── orderedAt
└── updatedAt

OrderItem
├── id (PK)
├── orderId (FK -> Order)
├── productId (FK -> Product)
├── sellerId (FK -> Member)
├── productName (snapshot)
├── productPrice (snapshot)
├── quantity
└── subtotal

Evaluation Criteria

Base Score (70 points)

ItemPointsDetailed Criteria
Feature Implementation30 ptsRequirements met, working correctly
Code Quality20 ptsReadability, naming, consistency
Design10 ptsLayer separation, responsibility distribution, exception handling
Testing10 ptsTest coverage, test quality

Bonus Points (35 points)

ItemPoints
Docker Compose runnable+5 pts
Swagger/REST Docs documentation+5 pts
GitHub Actions CI+5 pts
Caching applied (Redis or local)+5 pts
Event-driven notification handling+5 pts
QueryDSL dynamic queries+5 pts
Multi-module structure (with Dependency Inversion)+5 pts

Deduction Factors

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

Submission Method

  1. Upload 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 access API documentation
    • Project structure description
    • Additional implementations
  3. Submit the Repository URL

Notes

Execution Environment

Single Module

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

# Docker Compose execution
docker-compose up -d

Multi-Module

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

# JAR build
./gradlew :marketplace-api:bootJar

# Docker Compose execution
docker-compose up -d

Test Accounts (Seed Data)

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

Questions

  • Contact via email for questions during the assignment
  • If requirements are ambiguous, make reasonable decisions, implement accordingly, and specify in README

Checklist

Please verify before submission:

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

Hints

Implementation Order Recommendation (Single Module)
  1. Project Setup: Dependencies, profile separation, Docker Compose
  2. Domain Design: Entity, Repository
  3. Authentication Implementation: Spring Security, JWT
  4. Member API: Registration, login, my info
  5. Product API: CRUD, image upload
  6. Order API: Creation, retrieval, status changes
  7. Search/Pagination: Product search, filtering
  8. Caching/Events: Popular product caching, notification events
  9. Test Writing: Unit/integration tests
  10. Documentation: Swagger setup, README writing
Implementation Order Recommendation (Multi-Module)
  1. Project Structure Setup: settings.gradle, build.gradle for each module
  2. common Module: Common exceptions, ErrorCode, utilities
  3. domain Module: Entity, Repository interface, Service
  4. infra Module: Repository implementation, JPA configuration
  5. api Module: Controller, Security, Swagger
  6. Integration Testing: Full flow testing from the api module
  7. Docker Setup: Multi-module build Dockerfile
  8. Documentation: README including module structure diagram

Note: Be careful to avoid circular dependencies after module separation

Concurrency Handling Hint

Solutions for concurrency issues during inventory deduction:

// 1. Pessimistic Lock
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT p FROM Product p WHERE p.id = :id")
Optional<Product> findByIdWithLock(@Param("id") Long id);

// 2. Optimistic Lock + Retry
@Version
private Long version;
Event Handling Hint
// Publish event after order creation
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleOrderCreated(OrderCreatedEvent event) {
    // Handle notification asynchronously
    notificationService.notifySeller(event.getSellerId(), event.getOrderId());
}
Multi-Module Structure Hint

There are two approaches for multi-module:

OptionService LocationRepository HandlingCharacteristics
Option A (Standard)domainInterface/implementation separationStrict DIP application
Option B (Simplified)apiDirect JpaRepository usagePragmatic, less code

settings.gradle

rootProject.name = 'marketplace'

include 'marketplace-api'
include 'marketplace-domain'
include 'marketplace-infra'
include 'marketplace-common'

Module-specific build.gradle dependencies

// marketplace-common: No dependencies (common utilities, exceptions)

// marketplace-domain
dependencies {
    implementation project(':marketplace-common')
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
}

// marketplace-infra
dependencies {
    implementation project(':marketplace-common')
    implementation project(':marketplace-domain')
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    // QueryDSL (optional)
    implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
    runtimeOnly 'com.h2database:h2'
    runtimeOnly 'com.mysql:mysql-connector-j'
}

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

Option A: Repository Interface/Implementation Separation (DIP)

// marketplace-domain/.../ProductRepository.java (interface)
public interface ProductRepository {
    Product save(Product product);
    Optional<Product> findById(Long id);
}

// marketplace-infra/.../ProductRepositoryImpl.java (implementation)
@Repository
@RequiredArgsConstructor
public class ProductRepositoryImpl implements ProductRepository {
    private final ProductJpaRepository jpaRepository;

    @Override
    public Product save(Product product) {
        return jpaRepository.save(product);
    }
}

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(...) = queryFactory.selectFrom(product).where(...).fetch()
}

// marketplace-api/.../ProductService.kt (Service is located in the api module)
@Service
class ProductService(
    private val productJpaRepository: ProductJpaRepository  // Direct injection
) { ... }

Component Scan Configuration

// Application.java in marketplace-api
@SpringBootApplication(scanBasePackages = "com.example")
public class MarketplaceApplication { }
Multi-Module Docker Build Hint
FROM gradle:8.5-jdk17 AS builder
WORKDIR /app

# Copy Gradle files first (caching)
COPY build.gradle settings.gradle ./
COPY gradle ./gradle
COPY marketplace-common/build.gradle ./marketplace-common/
COPY marketplace-domain/build.gradle ./marketplace-domain/
COPY marketplace-infra/build.gradle ./marketplace-infra/
COPY marketplace-api/build.gradle ./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:17-jre-alpine
WORKDIR /app
COPY --from=builder /app/marketplace-api/build/libs/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

View Implementation Code

Good Luck!

Previous: Part 7 - Advanced Patterns Back to Start: Part 1 - Core Application Layer

This post is part of the Coupang Partners program, and a commission is earned from qualifying purchases.