Skip to main content

Overview

TechStore’s shopping cart system provides a persistent, user-specific cart that stores items before purchase. The cart seamlessly converts to orders during checkout, automatically managing stock validation and order creation.
Carts are persistent and user-specific. Each user has one active cart that survives across sessions.

Cart Model

The cart system uses two entities:
@Entity
public class Carrito {
    private Long id;
    
    @OneToOne
    private Usuario usuario;
    
    @OneToMany(mappedBy = "carrito", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<CarritoItem> items = new ArrayList<>();
}
@Entity
public class CarritoItem {
    private Long id;
    
    @ManyToOne
    private Carrito carrito;
    
    @ManyToOne
    private Producto producto;
    
    private Integer cantidad;
}
The orphanRemoval = true setting ensures that when items are removed from the cart, they’re automatically deleted from the database.

Getting a User’s Cart

GET /api/carrito/{usuarioId}
Authorization: Bearer {token}
Returns the user’s active cart. If no cart exists, one is automatically created:
@GetMapping("/{usuarioId}")
public ResponseEntity<Carrito> getCarrito(@PathVariable Long usuarioId) {
    return ResponseEntity.ok(carritoService.obtenerOCrearCarrito(usuarioId));
}
Implementation:
@Transactional
public Carrito obtenerOCrearCarrito(Long usuarioId) {
    return carritoRepository.findByUsuarioId(usuarioId).orElseGet(() -> {
        Usuario usuario = usuarioRepository.findById(usuarioId)
            .orElseThrow(() -> new RuntimeException("Usuario no encontrado"));
        
        Carrito nuevoCarrito = new Carrito();
        nuevoCarrito.setUsuario(usuario);
        return carritoRepository.save(nuevoCarrito);
    });
}
Response Example:
{
  "id": 12,
  "usuario": {
    "id": 5,
    "nombre": "John Doe",
    "email": "john@example.com"
  },
  "items": [
    {
      "id": 45,
      "producto": {
        "id": 10,
        "nombre": "MacBook Pro",
        "precio": 1999.99,
        "imagenUrl": "..."
      },
      "cantidad": 1
    },
    {
      "id": 46,
      "producto": {
        "id": 15,
        "nombre": "Magic Mouse",
        "precio": 79.99,
        "imagenUrl": "..."
      },
      "cantidad": 2
    }
  ]
}

Adding Items to Cart

POST /api/carrito/{usuarioId}/agregar?productoId={id}&cantidad={qty}
Authorization: Bearer {token}
Example: POST /api/carrito/5/agregar?productoId=10&cantidad=2 Adds a product to the cart. If the product already exists in the cart, the quantity is increased:
@PostMapping("/{usuarioId}/agregar")
public ResponseEntity<Carrito> agregar(
    @PathVariable Long usuarioId,
    @RequestParam Long productoId,
    @RequestParam Integer cantidad
) {
    return ResponseEntity.ok(
        carritoService.agregarProducto(usuarioId, productoId, cantidad)
    );
}
Implementation:
@Transactional
public Carrito agregarProducto(Long usuarioId, Long productoId, Integer cantidad) {
    Carrito carrito = obtenerOCrearCarrito(usuarioId);
    Producto producto = productRepository.findById(productoId)
        .orElseThrow(() -> new RuntimeException("Producto no encontrado"));
    
    // Check if product already in cart
    carrito.getItems().stream()
        .filter(item -> item.getProducto().getId().equals(productoId))
        .findFirst()
        .ifPresentOrElse(
            // If exists, increment quantity
            item -> item.setCantidad(item.getCantidad() + cantidad),
            // If new, add to cart
            () -> {
                CarritoItem nuevoItem = new CarritoItem();
                nuevoItem.setProducto(producto);
                nuevoItem.setCantidad(cantidad);
                nuevoItem.setCarrito(carrito);
                carrito.getItems().add(nuevoItem);
            }
        );
    
    return carritoRepository.save(carrito);
}
The system automatically handles both new items and quantity updates for existing items in a single operation.

Removing Items from Cart

DELETE /api/carrito/{usuarioId}/eliminar/{productoId}
Authorization: Bearer {token}
Example: DELETE /api/carrito/5/eliminar/10 Removes a specific product from the cart:
@DeleteMapping("/{usuarioId}/eliminar/{productoId}")
public ResponseEntity<Carrito> eliminarProducto(
    @PathVariable Long usuarioId,
    @PathVariable Long productoId
) {
    return ResponseEntity.ok(
        carritoService.eliminarProducto(usuarioId, productoId)
    );
}
Implementation:
@Transactional
public Carrito eliminarProducto(Long usuarioId, Long productoId) {
    Carrito carrito = carritoRepository.findByUsuarioId(usuarioId)
        .orElseThrow(() -> new RuntimeException(
            "Carrito no encontrado para el usuario: " + usuarioId
        ));
    
    // Remove item from collection
    boolean removido = carrito.getItems().removeIf(item ->
        item.getProducto().getId().equals(productoId)
    );
    
    if (removido) {
        // orphanRemoval = true automatically deletes the item
        return carritoRepository.save(carrito);
    } else {
        throw new RuntimeException("El producto no estaba en el carrito");
    }
}
To update quantity without removing, use the add endpoint with the desired final quantity difference.

Clearing the Cart

DELETE /api/carrito/{usuarioId}/limpiar
Authorization: Bearer {token}
Removes all items from the cart:
@DeleteMapping("/{usuarioId}/limpiar")
public ResponseEntity<Void> limpiar(@PathVariable Long usuarioId) {
    carritoService.limpiarCarrito(usuarioId);
    return ResponseEntity.ok().build();
}
Implementation:
@Transactional
public void limpiarCarrito(Long usuarioId) {
    carritoRepository.findByUsuarioId(usuarioId).ifPresent(carrito -> {
        carrito.getItems().clear();
        carritoRepository.save(carrito);
    });
}

Checkout Process

The checkout converts the shopping cart into a finalized order:
POST /api/carrito/{usuarioId}/checkout
Authorization: Bearer {token}
Example: POST /api/carrito/5/checkout
@PostMapping("/{usuarioId}/checkout")
public ResponseEntity<?> finalizarCompra(@PathVariable Long usuarioId) {
    try {
        Pedido pedido = carritoService.convertirCarritoEnPedido(usuarioId);
        return ResponseEntity.ok(pedido);
    } catch (RuntimeException e) {
        return ResponseEntity.badRequest().body(e.getMessage());
    }
}

Checkout Implementation

1

Retrieve cart

Load the user’s cart with all items
2

Validate cart not empty

Ensure cart has at least one item
3

Convert to order

Map CarritoItem objects to PedidoDetalle objects
4

Process order

Call order service to validate stock, calculate total, send email
5

Clear cart

Empty the cart after successful order creation
6

Return order

Return the completed order object to the client
Code Implementation:
@Transactional
public Pedido convertirCarritoEnPedido(Long usuarioId) {
    Carrito carrito = carritoRepository.findByUsuarioId(usuarioId)
        .orElseThrow(() -> new RuntimeException("Carrito no encontrado"));
    
    if (carrito.getItems().isEmpty()) {
        throw new RuntimeException("El carrito está vacío");
    }
    
    // 1. Create new order
    Pedido nuevoPedido = new Pedido();
    nuevoPedido.setUsuario(carrito.getUsuario());
    
    // 2. Map cart items to order details
    List<PedidoDetalle> detalles = carrito.getItems().stream().map(item -> {
        PedidoDetalle detalle = new PedidoDetalle();
        detalle.setProducto(item.getProducto());
        detalle.setCantidad(item.getCantidad());
        detalle.setPedido(nuevoPedido);
        return detalle;
    }).collect(Collectors.toList());
    
    nuevoPedido.setDetalles(detalles);
    
    // 3. Execute order creation logic (stock validation, total calculation, email)
    Pedido pedidoFinalizado = pedidoService.createPedido(nuevoPedido);
    
    // 4. Clear the cart
    carrito.getItems().clear();
    carritoRepository.save(carrito);
    
    return pedidoFinalizado;
}
If stock validation fails during checkout, the entire transaction is rolled back and the cart remains intact.

Checkout Response

Successful checkout returns a complete order object:
{
  "id": 42,
  "usuario": { ... },
  "fechaPedido": "2026-03-05T14:32:10",
  "estado": "PENDIENTE",
  "total": 2159.97,
  "detalles": [
    {
      "id": 78,
      "producto": {
        "id": 10,
        "nombre": "MacBook Pro"
      },
      "cantidad": 1,
      "precioUnitario": 1999.99
    },
    {
      "id": 79,
      "producto": {
        "id": 15,
        "nombre": "Magic Mouse"
      },
      "cantidad": 2,
      "precioUnitario": 79.99
    }
  ]
}

Integration with Order System

The cart checkout seamlessly integrates with the order management system:
  • Stock Validation: Reuses order service logic to check product availability
  • Total Calculation: Automatically computes order total from current prices
  • Email Notification: Triggers purchase confirmation email
  • Stock Deduction: Reduces inventory for purchased items
  • Order Tracking: Creates trackable order with status management
Prices are captured at checkout time, not when items are added to cart. This ensures accurate pricing if products change price before purchase.

API Reference

MethodEndpointDescriptionAuth
GET/api/carrito/{usuarioId}Get user’s cartUSER
POST/api/carrito/{usuarioId}/agregarAdd item to cartUSER
DELETE/api/carrito/{usuarioId}/eliminar/{productoId}Remove item from cartUSER
DELETE/api/carrito/{usuarioId}/limpiarClear entire cartUSER
POST/api/carrito/{usuarioId}/checkoutConvert cart to orderUSER

User Flow Example

1

Browse products

Customer views product catalog and selects items
2

Add to cart

POST /api/carrito/5/agregar?productoId=10&cantidad=1
3

Continue shopping

Add more items: POST /api/carrito/5/agregar?productoId=15&cantidad=2
4

Review cart

GET /api/carrito/5 to display cart summary
5

Adjust quantities

Remove unwanted item: DELETE /api/carrito/5/eliminar/15
6

Checkout

POST /api/carrito/5/checkout creates order and clears cart

Best Practices

Client-Side Total

Calculate cart totals on the frontend for instant feedback, but always use server prices at checkout.

Stock Warnings

Check product availability before checkout and warn users of out-of-stock items.

Session Persistence

Cart data persists across sessions. Users can return later to complete purchases.

Price Updates

Prices are captured at checkout, so cart displays should show current prices, not cached values.

Error Handling

Common error scenarios:
Scenario: User attempts checkout with no itemsResponse: 400 Bad Request
"El carrito está vacío"
Always handle these error responses on the client side to provide helpful user feedback.

Build docs developers (and LLMs) love