A complete, hands-on study guide built from a beginner-friendly Spring Boot tutorial. By the end you'll understand why, how, and what Spring Boot is — and you'll have built a real REST API backend that powers an influencer's merchandise store, the same way companies like Walmart, Netflix, Amazon, and Google use Spring Boot in production.
- What You'll Build
- System Thinking: Understand the Problem First
- Designing the Database (ER Diagram)
- Designing the API Endpoints
- What Is Spring & Spring Boot?
- Project Setup
- Project Structure Explained
- Configuration & Connecting a Database
- Securing Secrets with
.env - The Layered (MVC) Architecture
- Entities (The Models)
- Entity Relationships
- Repositories
- Controllers
- Services (Business Logic)
- Testing the Product CRUD
- DTOs (Data Transfer Objects)
- Building the Order Feature
- Swagger / API Documentation
- Key Concepts Cheat Sheet
- Annotations Reference
- Take-Home Assignments
You're going to play the role of a backend engineer ("the hero") helping Mr. User — an influencer who wants to set up a merchandise store to sell things like t-shirts, mugs, and stickers.
The store needs to answer everyday business questions such as:
- What is the price of an item?
- When was an order placed?
- How many products are left in stock?
- What items are in a particular order (for refunds/disputes)?
From these requirements, two core concepts emerge that the whole app revolves around:
- Product — the things being sold
- Order — the revenue-generating transactions
Key mindset: Always start with system thinking. Understand the problem before touching code. If Spring Boot disappeared tomorrow and you had to use another framework, your thought process should stay the same. Framework choice is an implementation detail; the design isn't.
Break the requirements into abilities for each type of user.
As the creator/owner, you can:
- Create a product
- Read a product (and read all products)
- Update a product
- Delete a product
As a customer, you can:
- Read all products
- Read an individual product
- Place an order
- View order history
These abilities map directly onto CRUD operations (Create, Read, Update, Delete) and become your API endpoints later.
Convert requirements into tables. The tutorial uses Eraser for diagrams, but any ER tool works.
| Column | Type | Notes |
|---|---|---|
| id | primary key | auto-generated |
| name | string | |
| description | string | optional |
| price | decimal | decimal so you can store paise/cents |
| stock_quantity | integer | |
| created_at | timestamp | metadata |
| updated_at | timestamp | metadata |
(A category field is also tracked — e.g. apparel vs tech.)
| Column | Type | Notes |
|---|---|---|
| id | primary key | |
| customer_name | string | |
| customer_email | string | |
| status | string | could be an enum; kept as string for simplicity |
| total_amount | decimal | |
| created_at | timestamp |
Products and orders have no direct relationship on their own — you don't know which products belong to which orders. A junction table connects them.
| Column | Type | Notes |
|---|---|---|
| id | primary key | |
| order_id | FK → orders | which order this belongs to |
| product_id | FK → products | which product this is for |
| price_at_purchase | decimal | snapshot of price when bought (see below) |
| quantity | integer | how many of this product |
Why
price_at_purchase? Product prices change over time (sales, demand). If you want reliable analytics — e.g. "how much revenue did I make in March?" — you must store the price at the time of purchase, not the current price.
- One order → many order items → one-to-many
- One product → many order items (a product can appear in many orders while stock lasts) → one-to-many
The junction table resolves what would otherwise be a many-to-many relationship between products and orders.
Use plural nouns matching the table names, and standard HTTP methods.
Product endpoints (/api/products):
| Action | Method | Path |
|---|---|---|
| Create product | POST | /api/products |
| Update product | PUT | /api/products/{id} |
| Read all products | GET | /api/products |
| Read one product | GET | /api/products/{id} |
| Delete product | DELETE | /api/products/{id} |
Order endpoints (/api/orders):
| Action | Method | Path |
|---|---|---|
| Place order | POST | /api/orders |
| Get order by ID | GET | /api/orders/{id} |
| Get all orders | GET | /api/orders |
Now you have: requirements → DB design → endpoints. Time to bring in Spring.
- Cooking from scratch = writing everything yourself (find ingredients, cook, eat).
- Frozen/ready-made food = someone already did the heavy work; you just heat and eat.
Spring Boot is the frozen food. Common problems (database connections, authentication, REST communication) have already been solved by developers worldwide who packaged solutions into reusable libraries. You don't reinvent the wheel — you pick the right package, configure it, and focus on building your application.
An open-source framework (on GitHub) that provides core support for dependency injection, transaction management, web apps, data access, messaging, and more.
You're not the first person to need DB connections or auth — others solved it, so you install, configure, and use their work. Spring does the heavy lifting ("it's a bodybuilder lifting the 100 kg dumbbells for you — you just go to the gym"). Its logo is a green leaf.
- Spring Framework = the core; flexible but requires more setup.
- Spring Boot = takes an opinionated view of building Spring apps so you "get up and running as quickly as possible." It's the frozen-food convenience layer on top of Spring.
Enterprises (Netflix and others) use it for real reasons: it's productive and battle-tested across use cases — microservices, cloud, web apps, serverless, even generative AI.
The tutorial uses IntelliJ IDEA (free Community edition available) and the Spring Initializr-style new-project wizard.
Settings chosen:
- Name:
creator-store - Language: Java
- Build tool: Maven
- Group ID: your website in reverse (e.g.
in.anishwala/com.google) - JDK: default (install one if you don't have it)
- Config format: YAML (
.yml) — preferred for readability over.properties - Spring Boot version: latest stable
Starter dependencies added:
| Dependency | Purpose |
|---|---|
| PostgreSQL Driver | Connect the Java app to a Postgres database |
| Lombok | Auto-generate getters/setters/constructors (less boilerplate) |
| Spring Web | Create and work with REST APIs |
| Spring Data JPA | The ORM layer — converts Java classes ↔ DB tables |
| OpenAPI Spec (springdoc) | Generates Swagger API documentation (not OpenAI!) |
Need GraphQL instead of REST? Add "Spring for GraphQL". Dependencies are like the entries in a JavaScript
package.json.
After creating, do an initial git commit so you can track progress from the start.
| File / Folder | What it is |
|---|---|
.idea/ |
IntelliJ IDE-specific config — don't touch |
.mvn/ |
Maven wrapper config (where to find/download dependencies) |
mvnw / mvnw.cmd |
Maven Wrapper — Maven bundled in the project, so no separate install needed |
.gitignore |
Files/folders excluded from git |
pom.xml |
The Maven project file (XML, not JSON) — like package.json. Lists dependencies, group/artifact ID, Spring Boot version |
src/main/java |
Your actual source code |
src/main/resources/application.yml |
Central app configuration (DB credentials, flags, etc.) |
src/test |
Unit tests |
target/ |
Generated build output — compiled .class files |
| Main Application class | Entry point with the main method and a Run button |
When you first run it with the Postgres dependency but no DB configured, the app fails on purpose with a "missing datasource URL" error. That's a good sign — it proves the dependency is active and waiting for config.
Add database config to application.yml. You need three things: URL, username, password, plus
ORM (Hibernate) settings.
Options: Supabase, Neon, or a local Docker Postgres. The tutorial uses Supabase.
Supabase setup:
- Create a new project (name it
creator-store). - Generate and save the database password (you'll need it).
- Pick the closest region (lower latency).
- Click Connect → choose Session Pooler (not Direct Connection — direct connections aren't pooled and Supabase may reject them).
- Choose connection type JDBC (Java-friendly).
- Copy the connection string.
The URL starts with
jdbc:postgresql://...— never remove thejdbc:prefix; it's the protocol Java needs.
- JPA = Jakarta Persistence API (a standard Java gives for persistence).
- Hibernate = the ORM that implements JPA (Java's equivalent of Drizzle/Prisma/Mongoose).
Settings to enable:
ddl-auto: update— automatically apply schema changes to the DB. (DB queries split into DML, DCL, DDL; this is the Data Definition Language part.)show-sql: true— print the SQL Hibernate runs (great for debugging/learning).format_sql: true— make that SQL human-readable.
ORM benefit: You write Java; Hibernate generates all the SQL —
CREATE TABLE,INSERT,SELECT, foreign-key constraints — automatically. You won't write a single SQL line yourself.
Never hardcode credentials in application.yml — once committed to git they're exposed to the
world.
Steps:
- Add the
dotenv-javadependency (search Maven Repository — the Java equivalent of npmjs.com — pick the latest version, copy the install snippet intopom.xml, and sync Maven). - Create a
.envfile in the project root with key–value pairs:DATABASE_URL=jdbc:postgresql://... DATABASE_USERNAME=... DATABASE_PASSWORD=... - When prompted, tell git not to track
.env(it holds secrets). - Load
.envinto system properties in your main class so Spring can read it:This loops over eachDotenv dotenv = Dotenv.configure().ignoreIfMissing().load(); dotenv.entries().forEach(entry -> System.setProperty(entry.getKey(), entry.getValue()));
.enventry and sets it as a system property of the running Java process. - Reference the variables in
application.ymlwith${...}syntax:spring: datasource: url: ${DATABASE_URL} username: ${DATABASE_USERNAME} password: ${DATABASE_PASSWORD}
Now application.yml is safe to commit (no secrets), and in production you set the env vars on the
server. Run the app — you should see a successful DB connection log and the app running on
port 8080.
Commit message convention used:
chore: add .env (dotenv-java) for the application.
Spring Boot apps follow a clean separation of concerns. A request flows through layers:
Client (browser / mobile app / Postman)
│
▼
Controller ← defines REST endpoints, handles HTTP. Single Responsibility: just route.
│
▼
Service ← the "protein": all business logic lives here.
│
▼
Repository ← talks to the database (CRUD). Powered by Hibernate.
│
▼
Entity ← Java classes mapped to DB tables.
│
▼
Database (PostgreSQL)
Why separate layers? To follow SOLID principles. The controller should only map endpoints and hand off work — never hold business logic. Otherwise a controller with 5+ methods becomes bloated and unmaintainable.
Packages you'll create: entities, repositories, controllers, services, and later dto.
Create a package entities with three classes: Product, Order, OrderItem.
Fields: id (long), name, description, category, price (BigDecimal), stockQuantity
(integer).
Class-level annotations:
@Entity— marks this as a JPA entity (from Jakarta Persistence API).@Table(name = "products")— sets the table name (plural).@Getter/@Setter— Lombok generates accessors.@AllArgsConstructor/@NoArgsConstructor— Lombok constructors.@Builder— Lombok enables the Builder design pattern (create objects fluently withoutnew).
Field-level annotations:
@Id+@GeneratedValue(strategy = GenerationType.IDENTITY)— primary key, DB generates the sequence.@Column(nullable = false)— column constraints.@Column(name = "stock_quantity")— map camelCase Java fields to snake_case DB columns.@DecimalMin(value = "0", inclusive = false, message = "Price must be greater than zero")— validation forprice.@Min(value = 0, message = "Stock cannot be less than zero")— for integers (use@Min, not@DecimalMin).@NotBlank(message = "Product name is required")— string can't be empty.@NotNull(message = "Price is required")— value must be present.
Annotations attach metadata that extends a class's functionality without you writing the plumbing. They work at both class level (what the table is) and field level (constraints and validations).
When you run the app, Hibernate auto-creates the tables and foreign-key constraints. Verify in the Supabase Table Editor and Schema Visualizer — it matches your original ER diagram, with zero hand-written SQL.
This is where Spring Boot makes something that's painful in other frameworks remarkably simple.
In Order:
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
@JsonManagedReference
private List<OrderItem> orderItems;In OrderItem:
@ManyToOne
@JoinColumn(name = "order_id", nullable = false)
@JsonBackReference
private Order order;In Product:
@OneToMany(mappedBy = "product")
@JsonIgnore
private List<OrderItem> orderItems;In OrderItem:
@ManyToOne
@JoinColumn(name = "product_id", nullable = false)
private Product product;Why the JSON annotations?
@JsonIgnoreon Product'sorderItems— when you fetch a product you want its own fields, not every order it ever appeared in. This prevents accidentally dragging along huge nested data.@JsonManagedReference/@JsonBackReference— prevent infinite recursion when serializing the two-way Order↔OrderItem relationship to JSON.cascade = CascadeType.ALL— saving an order also saves its order items.
@PrePersist — a method that runs before a record is first saved, used to set createdAt:
@PrePersist
public void prePersist() {
this.createdAt = LocalDateTime.now();
}Create a repositories package. Repositories are interfaces (not classes!) that talk to the
database. Each entity gets one.
public interface ProductRepository extends JpaRepository<Product, Long> {
}- Extend
JpaRepository<EntityType, IdType>— here<Product, Long>because Product'sidis aLong. - You write no method bodies. Hibernate provides the implementations of JPA's standard methods:
save(),findAll(),findById(),deleteById(), and more — all "cooked in" out of the box. - Because you depend on the JPA standard (not a specific ORM), swapping ORMs later means changing
config in
application.yml, not your code.
Create three: ProductRepository, OrderRepository, OrderItemRepository.
Create a controllers package. Controllers define the REST endpoints clients (frontend, mobile,
Postman) talk to.
@RestController
@RequestMapping("/api/products")
@RequiredArgsConstructor
public class ProductController {
private final ProductService productService; // dependency injection
@PostMapping
public Product createProduct(@Valid @RequestBody Product product) {
return productService.createProduct(product);
}
@PutMapping("/{id}")
public Product updateProduct(@PathVariable Long id,
@Valid @RequestBody Product product) {
return productService.updateProduct(id, product);
}
@GetMapping
public List<Product> getProducts() {
return productService.getProducts();
}
@GetMapping("/{id}")
public Product getProductById(@PathVariable Long id) {
return productService.getProductById(id);
}
@DeleteMapping("/{id}")
public void deleteProduct(@PathVariable Long id) {
productService.deleteProduct(id);
}
}Annotations explained:
@RestController— marks the class for REST API operations.@RequestMapping("/api/products")— base path; every method lives under it.@PostMapping,@PutMapping,@GetMapping,@DeleteMapping— map HTTP methods.@RequestBody— read data from the request body (for objects you can't fit in a URL).@PathVariable— read a value from the URL path (e.g. the{id}). The path variable name must match the method parameter.@Valid— triggers the validations you defined on the entity/DTO.@RequiredArgsConstructor(Lombok) — generates a constructor for allfinalfields, enabling dependency injection of the service.
Naming matters: use clear, readable method names (
getProductById) so anyone reading the codebase instantly understands the intent.
The controller follows the Single Responsibility Principle — it only maps endpoints and hands work off to the service.
Create a services package. Annotate with @Service so Spring registers it as a bean
available for injection. Inject the repository via @RequiredArgsConstructor + a private final
field.
@Service
@RequiredArgsConstructor
public class ProductService {
private final ProductRepository productRepository;
public Product createProduct(Product product) {
return productRepository.save(product);
}
public Product updateProduct(Long id, Product product) {
Product existing = productRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Product not found with ID " + id));
existing.setName(product.getName());
existing.setDescription(product.getDescription());
existing.setCategory(product.getCategory());
existing.setPrice(product.getPrice());
existing.setStockQuantity(product.getStockQuantity());
return productRepository.save(existing);
}
public List<Product> getProducts() {
return productRepository.findAll();
}
public Product getProductById(Long id) {
return productRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Product not available"));
// Alternatively, return Optional<Product> and let the caller handle absence.
}
public void deleteProduct(Long id) {
productRepository.deleteById(id);
}
}Key points:
- Always check existence before updating — never blindly update an ID that may not exist.
findByIdreturns anOptional— either handle it with.orElseThrow(...)or return theOptionalto the caller.- Getters/setters come from Lombok.
- The service is the only layer that talks to the repository when persistence is needed.
Dependency Injection (DI): instead of creating objects yourself with
new, you declare what you need (afinalfield) and Spring "injects" it for you. This is a core Spring concept.
That completes the full MVC flow for Product: Model → Repository → Controller → Service → DB.
Use an API client — the tutorial uses Requestly (Postman or any client works). Run the app
first (confirm DB connection + port 8080), then create a collection creator-store.
Create a product (POST http://localhost:8080/api/products):
{
"name": "T-Shirt",
"stockQuantity": 100,
"price": 199,
"category": "apparel"
}Description is optional, so omitting it still works. Response returns the object with an
auto-generated id of 1.
Read all (GET /api/products): returns a JSON array of product objects.
Read one (GET /api/products/2): pass the ID as a path variable; returns one product.
Update (PUT /api/products/2): change the mapping to PUT, send the new fields in the body
(e.g. bump price to 150, quantity to 9000). Confirm with a fresh GET.
Persistence check: stop and restart the app — the data is still there because it lives in the database, not memory.
Delete (DELETE /api/products/2): no body needed; removes the product. A subsequent GET shows
it's gone.
That's the full CRUD lifecycle on Product.
When accepting an order, you don't want clients sending the entity's complex relationships. A DTO exposes only the fields you actually need.
Create a dto package.
@Getter @Setter
@NoArgsConstructor @AllArgsConstructor @Builder
public class OrderItemRequest {
@NotNull(message = "Product ID is required")
private Long productId;
@NotNull(message = "Quantity is required")
@Min(value = 1, message = "Quantity must be at least one")
private Integer quantity;
}@Getter @Setter
@NoArgsConstructor @AllArgsConstructor @Builder
public class OrderRequest {
@NotBlank(message = "Customer name is required")
private String customerName;
@NotBlank(message = "Customer email is required")
@Email(message = "Enter a valid email")
private String customerEmail;
@Valid
@NotEmpty(message = "Order must contain at least one item")
private List<OrderItemRequest> items;
}Why DTOs? They keep request payloads clean and free of entity relationships, and let you apply validations specific to incoming requests.
Don't forget the
@JsonManagedReference/@JsonBackReference+cascade = CascadeType.ALLwiring on theOrder/OrderItementities so saving an order cascades to its items.
The OrderService is where the whole app comes together. Inject both repositories:
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final ProductRepository productRepository;
@Transactional
public Order createOrder(OrderRequest orderRequest) {
Order order = new Order();
order.setCustomerName(orderRequest.getCustomerName());
order.setCustomerEmail(orderRequest.getCustomerEmail());
order.setStatus("CONFIRMED");
List<OrderItem> orderItems = new ArrayList<>();
BigDecimal totalPrice = BigDecimal.ZERO;
for (OrderItemRequest itemRequest : orderRequest.getItems()) {
// 1. Verify the product exists
Product product = productRepository.findById(itemRequest.getProductId())
.orElseThrow(() -> new RuntimeException(
"Product not found with ID " + itemRequest.getProductId()));
// 2. Check stock availability
if (product.getStockQuantity() < itemRequest.getQuantity()) {
throw new RuntimeException("Not enough stock for this product");
}
// 3. Calculate line price (current price × quantity) and add to total
BigDecimal priceOfItem = product.getPrice()
.multiply(BigDecimal.valueOf(itemRequest.getQuantity()));
totalPrice = totalPrice.add(priceOfItem);
// 4. Reduce stock and persist the product
product.setStockQuantity(product.getStockQuantity() - itemRequest.getQuantity());
productRepository.save(product);
// 5. Build the order item (junction record) using the Builder pattern
OrderItem orderItem = OrderItem.builder()
.order(order)
.product(product)
.quantity(itemRequest.getQuantity())
.priceAtPurchase(product.getPrice())
.build();
orderItems.add(orderItem);
}
order.setOrderItems(orderItems);
order.setTotalPrice(totalPrice);
return orderRepository.save(order);
}
}Placing an order does multiple SQL operations: insert into orders, insert into order_items,
and update products (stock). Grouping them in a single transaction (from Jakarta Persistence
API) means: if anything fails, everything rolls back; if all succeed, it commits. No
half-finished orders.
Watch out: the tutorial notes a common mistake — initially the service returned the in-memory order object instead of calling
orderRepository.save(order). Always persist through the repository. Debugging like this is normal and expected.
@RestController
@RequestMapping("/api/orders")
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService;
@PostMapping
public Order createOrder(@Valid @RequestBody OrderRequest orderRequest) {
return orderService.createOrder(orderRequest);
}
}Test it (POST http://localhost:8080/api/orders):
{
"customerName": "Anirudh",
"customerEmail": "anirudh@gmail.com",
"items": [
{ "productId": 1, "quantity": 5 }
]
}The response returns the order with customer info, status, a dynamically calculated total
price, the linked order items, the related products, and a createdAt timestamp.
The springdoc-openapi dependency (added at project creation) auto-generates interactive API docs.
- Confirm the dependency is in
pom.xml(search "Spring Doc" / OpenAPI specification). - Run the app and visit:
http://localhost:8080/swagger-ui/index.html
You'll see a (dark-mode) UI listing all controllers and their CRUD operations. You can:
- Expand an endpoint, click Try it out, enter an ID/body, and Execute — e.g. GET product
ID 1 returns a
200with the t-shirt details. - For POST/PUT, Swagger auto-generates an example request body based on your DTOs and path variables — no guessing the format (unlike raw Postman/Requestly).
As a backend developer, you hand this Swagger documentation to the frontend team so they can test your APIs without you explaining every payload.
| Concept | Meaning |
|---|---|
| Spring Framework | Open-source core framework: dependency injection, transactions, web, data access, messaging. |
| Spring Boot | Opinionated convenience layer on Spring — fast setup, sensible defaults. |
| ORM | Object-Relational Mapping — converts Java objects ↔ DB tables. |
| JPA | Jakarta Persistence API — the standard Java provides for persistence. |
| Hibernate | The ORM that implements JPA and writes the SQL for you. |
| Entity | A Java class mapped to a database table. |
| Repository | Interface (extends JpaRepository) that gives CRUD methods for free. |
| Controller | Defines REST endpoints; single responsibility = routing. |
| Service | Holds business logic; talks to repositories. |
| DTO | Data Transfer Object — clean request/response shapes without entity relationships. |
| Dependency Injection | Spring supplies your objects (beans) instead of you using new. |
| Bean | An object managed by the Spring container. |
| Junction table | Resolves many-to-many by linking two tables (here order_items). |
| Transaction | A group of DB operations that all succeed or all roll back. |
| Builder pattern | Fluent object creation without the new keyword (via Lombok @Builder). |
| Lombok | Library that auto-generates getters/setters/constructors/builders. |
| Maven | Build tool & dependency manager (pom.xml); analog of npm/package.json. |
| Annotation | Where | Purpose |
|---|---|---|
@Entity |
class | Marks a JPA entity. |
@Table(name="...") |
class | Sets the DB table name. |
@Id |
field | Marks the primary key. |
@GeneratedValue(strategy = GenerationType.IDENTITY) |
field | DB auto-generates the ID. |
@Column(nullable=false, name="...") |
field | Column constraints / snake_case name. |
@NotBlank / @NotNull / @NotEmpty |
field | Presence validations. |
@Min / @DecimalMin |
field | Numeric minimums (@Min for ints, @DecimalMin for decimals). |
@Email |
field | Validates email format. |
@PrePersist |
method | Runs before first save (e.g. set createdAt). |
@OneToMany(mappedBy=..., cascade=...) |
field | One-to-many relationship. |
@ManyToOne |
field | Many-to-one relationship. |
@JoinColumn(name=..., nullable=false) |
field | Foreign-key column. |
@JsonIgnore |
field | Exclude field from JSON output. |
@JsonManagedReference / @JsonBackReference |
field | Prevent infinite recursion in bidirectional JSON. |
@Getter / @Setter |
class | Lombok accessors. |
@NoArgsConstructor / @AllArgsConstructor |
class | Lombok constructors. |
@Builder |
class | Lombok builder pattern. |
@RequiredArgsConstructor |
class | Lombok constructor for final fields (DI). |
@RestController |
class | REST API controller. |
@RequestMapping("/path") |
class | Base path for endpoints. |
@PostMapping / @GetMapping / @PutMapping / @DeleteMapping |
method | Map HTTP verbs. |
@RequestBody |
param | Bind the request body to an object. |
@PathVariable |
param | Bind a URL path segment to a parameter. |
@Valid |
param | Trigger bean validation. |
@Service |
class | Marks a service bean. |
@Transactional |
method | Run inside a DB transaction. |
The tutorial leaves these for you to implement (take a screenshot when done and share it):
- Get all orders —
GET /api/orders. AddgetOrders()returningList<Order>in the service (currently aTODOreturningnull) and wire it to the controller. - Get order by ID —
GET /api/orders/{id}. Returns a singleOrder(useful for order tracking). Add the supporting service method and wire it up. - Add a
@PrePersistcreatedAtto other entities (the pattern was only shown onOrder). - Add extra validations to the
Orderentity (none were added beyond the basics). - Homework lookups: Google these terms to solidify understanding — JPA, DDL/DML/DCL,
Builder design pattern, getters/setters,
.classfiles, dependency injection.
- Commit often with conventional messages (
chore:,feat:, etc.) so your git history tells the story of how the project was built. - The exact same design thinking (requirements → DB → endpoints → layers) applies no matter which framework you use — Spring Boot just makes the implementation faster and cleaner.
- Real production systems at Netflix, Amazon, Walmart, and Google are built on these same fundamentals.
Happy building! 🌱