DEV Community

Cover image for How I Built a Full-Stack Bookstore App in 10 Days (And What I Learned)
Minotaur
Minotaur

Posted on

How I Built a Full-Stack Bookstore App in 10 Days (And What I Learned)

How I Built a Full-Stack Bookstore App in 10 Days (And What I Learned)

The Challenge

Ten days. That's what I had to build a production-ready online bookstore from scratch as part of Amazon's ATLAS training program.

The requirements seemed straightforward at first: users should be able to browse books, add them to a cart, and checkout. But then came the "nice-to-haves" that quickly became must-haves in my mind:

  • Personalized recommendations
  • Browsing history
  • Admin panel with bulk operations
  • An inventory system inspired by Amazon's ASIN

Here's what I learned building it—and the bugs that almost broke me.


The Tech Stack

Before diving into code, let me share what I used:

Backend:

  • Java 17 + Spring Boot 3
  • DynamoDB (with Local for development)
  • Maven

Frontend:

  • React 18 + Vite
  • Material-UI v5
  • React Router v6

DevOps:

  • GitHub Actions for CI/CD
  • Docker for DynamoDB Local

Why this stack? Spring Boot gave me rapid development with built-in dependency injection. DynamoDB forced me to think in NoSQL patterns (no JOINs allowed!). React + MUI meant I could focus on functionality rather than CSS battles.


Challenge #1: The Duplicate Books Nightmare

The Problem

Day 3. I'm feeling good. The admin panel works, bulk upload is functional, and I can import 100 books from a CSV file.

Then I run the import again.

Now I have 200 books. The same 100 books, duplicated.

The Hobbit (ID: a1b2c3d4)
The Hobbit (ID: e5f6g7h8)  // Same book, different ID!
The Hobbit (ID: i9j0k1l2)  // I ran it three times...
Enter fullscreen mode Exit fullscreen mode

The culprit? Random UUIDs.

// The problematic code
public void saveBook(Book book) {
    if (book.getId() == null) {
        book.setId(UUID.randomUUID().toString()); // Different every time!
    }
    bookTable.putItem(book);
}
Enter fullscreen mode Exit fullscreen mode

The Solution: Deterministic IDs

I needed the same book to always get the same ID. The solution? Hash the book's "natural key" (title + author):

public class DeterministicId {

    public static String forBook(String title, String author) {
        String input = (title + "|" + author).toLowerCase().trim();
        byte[] hash = sha1(input);
        return "b-" + toHex(hash).substring(0, 12);
    }

    private static byte[] sha1(String input) {
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-1");
            return md.digest(input.getBytes(StandardCharsets.UTF_8));
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Now "The Hobbit" by "J.R.R. Tolkien" always generates b-7a3f2e1d9c8b. Import the same CSV 100 times? Still just one copy.

Key Takeaway: When you need idempotent operations, use deterministic identifiers based on business keys, not random UUIDs.


Challenge #2: Building an Amazon-Style ASIN System

Amazon uses ASINs (Amazon Standard Identification Numbers) like B0CHXLZ5P3 for every product. I wanted the same for my bookstore.

The Implementation

public class AsinGenerator {

    private static final String CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";

    public static String generateFromBook(String title, String author) {
        String input = (title + "|" + author).toLowerCase().trim();
        byte[] hash = sha256(input);

        StringBuilder sb = new StringBuilder("B0"); // Amazon format
        for (int i = 0; i < 8; i++) {
            int index = Math.abs(hash[i] % CHARS.length());
            sb.append(CHARS.charAt(index));
        }
        return sb.toString(); // e.g., "B0XK7M2N9P"
    }

    public static boolean isValid(String asin) {
        return asin != null 
            && asin.length() == 10 
            && asin.startsWith("B0")
            && asin.matches("[A-Z0-9]{10}");
    }
}
Enter fullscreen mode Exit fullscreen mode

Why deterministic? Same reason as IDs—I wanted the same book to always get the same ASIN. This enables:

  • Bulk updates by ASIN (not just internal ID)
  • Human-readable identifiers for admin operations
  • Collision probability of ~1 in 2.8 trillion (36^8)

Challenge #3: Browsing History with LinkedList

The requirement: "Show the user's 10 most recently viewed books."

This screamed LinkedList to me:

  • O(1) insertion at front
  • O(1) removal from end
  • Natural FIFO ordering
public class BrowsingHistoryService {

    private static final int MAX_CAPACITY = 10;

    public void addBookToHistory(String username, String bookId) {
        LinkedList<String> history = getHistory(username);

        // Remove if already exists (user viewed it before)
        history.remove(bookId);

        // Add to front (most recent)
        history.addFirst(bookId);

        // Maintain capacity
        while (history.size() > MAX_CAPACITY) {
            history.removeLast();
        }

        saveHistory(username, history);
    }
}
Enter fullscreen mode Exit fullscreen mode

The remove(bookId) before addFirst(bookId) handles a subtle edge case: if a user views the same book twice, it should move to the front, not appear twice.


Challenge #4: DynamoDB Said "No" to Empty Strings

Day 5. Everything works locally. I deploy, test the admin panel, and...

DynamoDbException: The AttributeValue for a key attribute 
cannot contain an empty string value.
Enter fullscreen mode Exit fullscreen mode

The Problem

When creating a book without providing an ID, my React frontend was sending:

const book = {
  id: '',  // Empty string, NOT null
  title: 'New Book',
  author: 'Some Author'
};
Enter fullscreen mode Exit fullscreen mode

DynamoDB's partition keys cannot be empty strings. Null? Fine. Undefined? Fine. Empty string? Absolutely not.

The Solution: Defense in Depth

I fixed it at both layers:

Frontend:

const bookToUpload = {
  id: book.id?.trim() || undefined,  // Convert empty to undefined
  title: book.title,
  author: book.author,
};
Enter fullscreen mode Exit fullscreen mode

Backend:

@PostMapping
public ResponseEntity<?> createBook(@RequestBody Book book) {
    if (book.getId() == null || book.getId().isBlank()) {
        book.setId(DeterministicId.forBook(
            book.getTitle(), 
            book.getAuthor()
        ));
    }
    adapter.save(book);
    return ResponseEntity.created(uri).build();
}
Enter fullscreen mode Exit fullscreen mode

Key Takeaway: Always validate at API boundaries. What seems like "null" in one language might be an empty string in another.


Challenge #5: Book Covers Were Getting Cropped

I integrated OpenLibrary's free API for book covers:

const coverUrl = `https://covers.openlibrary.org/b/isbn/${isbn}-L.jpg`;
Enter fullscreen mode Exit fullscreen mode

But my covers looked terrible. Half the title was cut off on every book.

The Problem

/* This was cropping images */
img {
  width: 100%;
  height: 300px;
  object-fit: cover; /* Fills space, crops excess */
}
Enter fullscreen mode Exit fullscreen mode

The Solution

Amazon doesn't crop their product images—they show the full cover with padding:

<Box sx={{
  height: 380,
  background: '#ffffff',
  padding: 3,
  display: 'flex',
  alignItems: 'center',
  justifyContent: 'center',
}}>
  <img 
    src={coverUrl}
    style={{
      maxWidth: '100%',
      maxHeight: '100%',
      objectFit: 'contain', // Shows FULL image
    }}
    onError={() => setShowFallback(true)}
  />
</Box>
Enter fullscreen mode Exit fullscreen mode

For books without ISBNs, I created gradient fallbacks based on genre:

const genreGradients = {
  Fantasy: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
  Programming: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)',
  Mystery: 'linear-gradient(135deg, #2c3e50 0%, #4a00e0 100%)',
};
Enter fullscreen mode Exit fullscreen mode

The CI/CD Pipeline That Saved Me

On Day 9, I set up GitHub Actions.

jobs:
  backend-build:
    runs-on: ubuntu-latest
    services:
      dynamodb:
        image: amazon/dynamodb-local:latest
        ports:
          - 8000:8000

    steps:
      - uses: actions/checkout@v4

      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'
          cache: maven

      - name: Run tests
        run: mvn test

      - name: Build
        run: mvn package -DskipTests
Enter fullscreen mode Exit fullscreen mode

Why this mattered: I pushed a "quick fix" that broke 12 tests. Without CI, I would've deployed broken code. Instead, GitHub showed a red and I fixed it before it became a problem.


Final Stats

After 10 days:

Metric Value
Backend Classes 42+
Frontend Components 25+
Lines of Code ~6,300
Tests 134 (passing)
Test Coverage 58%
API Endpoints 20+
Git Commits 100+

What I'd Do Differently

  1. Start with CI/CD on Day 1, not Day 9
  2. Write tests as I code, not at the end
  3. Use TypeScript for the frontend (caught several bugs that types would've prevented)
  4. Design the DynamoDB schema upfront (migrations are painful)

Key Takeaways

  1. Deterministic IDs > Random UUIDs when you need idempotent operations
  2. Validate at API boundaries—empty strings and null are not the same
  3. object-fit: contain for product images, cover for backgrounds
  4. LinkedList is perfect for fixed-capacity, ordered collections
  5. CI/CD catches bugs before users do

Try It Yourself

The full source code is on GitHub: Online Bookstore Application

Feel free to clone it, break it, and make it better. That's how we learn.


What's the most challenging bug you've faced in a project? I'd love to hear about it in the comments!

Top comments (0)