Skip to content

Order API

A Order API gerencia os pedidos do domínio store, permitindo criar e consultar ordens associadas ao usuário autenticado.
Ela segue o padrão adotado no projeto: interface (order) e service (order-service) atrás do gateway e protegidos por JWT.

Trusted layer e segurança

Toda requisição externa entra pelo gateway.
As rotas /order/** são protegidas: é obrigatório enviar Authorization: Bearer <jwt>.


Visão geral

  • Interface (order): define o contrato (DTOs e Feign) consumido por outros módulos/fronts.
  • Service (order-service): implementação REST, regras de negócio, persistência (JPA), e migrações (Flyway).
classDiagram
    namespace order {
        class OrderController {
            +create(OrderIn orderIn): OrderOut
            +findAll(): List<OrderOut>
            +findById(String id): OrderOut
        }

        class OrderIn {
            -List<OrderItemIn> items
        }

        class OrderItemIn {
            -String idProduct
            -int quantity
        }

        class OrderOut {
            -String id
            -String date
            -List<OrderItemOut> items
            -Double total
        }

        class OrderItemOut {
            -String id
            -ProductOut product
            -int quantity
            -Double total
        }
    }
    namespace order-service {
        class OrderResource {
            +create(OrderIn orderIn): OrderOut
            +findAll(): List<OrderOut>
            +findById(String id): OrderOut
        }

        class OrderService {
            +create(OrderIn orderIn): OrderOut
            +findAll(): List<OrderOut>
            +findById(String id): OrderOut
        }

        class OrderRepository {
            +save(Order order): Order
            +findAll(): List<Order>
            +findById(String id): Optional<Order>
        }

        class Order {
            -String id
            -String date
            -List~OrderItem~ items
        }

        class OrderItem {
            -String id
            -String idProduct
            -int quantity
            -Double total
        }

        class OrderModel {
            +toEntity(OrderIn orderIn): Order
            +toOut(Order order): OrderOut
        }
    }
    <<Interface>> OrderController
    OrderController ..> OrderIn
    OrderController ..> OrderOut

    <<Interface>> OrderRepository
    OrderController <|-- OrderResource
    OrderResource *-- OrderService
    OrderService *-- OrderRepository
    OrderService ..> Order
    OrderService ..> OrderModel
    OrderRepository ..> Order
    Order ..> OrderItem
    OrderOut ..> OrderItemOut
    OrderItemOut ..> ProductOut

Estrutura da requisição

flowchart LR
    subgraph api [Trusted Layer]
        direction TB
        gateway --> account
        gateway --> auth
        account --> db@{ shape: cyl, label: "Database" }
        auth --> account
        gateway --> product
        gateway e6@==> order:::red
        product --> db
        order e3@==> db
        order e4@==> product
    end
    internet e1@==>|request| gateway
    e1@{ animate: true }
    e3@{ animate: true }
    e4@{ animate: true }
    e6@{ animate: true }
    classDef red fill:#fcc
    click order "#order-api" "Order API"

Order

📁 api/
└── 📁 order/
    ├── 📁 src/
       └── 📁 main/
           └── 📁 java/
               └── 📁 store/
                   └── 📁 order/
                       ├── 📄 OrderController.java
                       ├── 📄 OrderIn.java
                       ├── 📄 OrderOut.java
                       ├── 📄 OrderItemIn.java
                       └── 📄 OrderItemOut.java
    ├── 📄 pom.xml
    └── 📄 Jenkinsfile
Source
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.5.5</version>
        <relativePath/>
    </parent>

    <groupId>store</groupId>
    <artifactId>order</artifactId>
    <version>1.0.0</version>

    <properties>
        <java.version>21</java.version>
        <spring-cloud.version>2025.0.0</spring-cloud.version>
        <maven.compiler.proc>full</maven.compiler.proc>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <!-- Reuso de DTO de produto no OrderItemOut -->
        <dependency>
            <groupId>store</groupId>
            <artifactId>product</artifactId>
            <version>${project.version}</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

</project>
pipeline {
    agent any

    stages {
        stage('Build') {
            steps {
                sh 'mvn -B -DskipTests clean install'
            }
        }
    }

}
package store.order;

import java.util.List;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@FeignClient(name = "order", url = "http://order:8080")
public interface OrderController {

    @PostMapping("/order")
    ResponseEntity<OrderOut> create(@RequestBody OrderIn in);

    @GetMapping("/order")
    ResponseEntity<List<OrderOut>> findAll();

    @GetMapping("/order/{id}")
    ResponseEntity<OrderOut> findById(@PathVariable("id") String id);
}
1
2
3
4
5
6
7
package store.order;

import java.util.List;
import lombok.Builder;

@Builder
public record OrderIn(List<OrderItemIn> items) {}
package store.order;

import java.util.List;
import lombok.Builder;

@Builder
public record OrderOut(
    String id,
    String date,
    List<OrderItemOut> items,
    Double total
) {}
1
2
3
4
5
6
package store.order;

import lombok.Builder;

@Builder
public record OrderItemIn(String idProduct, Integer quantity) {}
package store.order;

import lombok.Builder;
import store.product.ProductOut;

@Builder
public record OrderItemOut(
    String id,
    ProductOut product,
    Integer quantity,
    Double total
) {}
mvn clean install

Order-Service

📁 api/
└── 📁 order-service/
    ├── 📁 k8s/
       └── 📄 k8s.yaml
    ├── 📁 src/
       └── 📁 main/
           ├── 📁 java/
              └── 📁 store/
                  └── 📁 order/
                      ├── 📄 Order.java
                      ├── 📄 OrderItem.java
                      ├── 📄 OrderApplication.java
                      ├── 📄 OrderModel.java
                      ├── 📄 OrderParser.java
                      ├── 📄 OrderRepository.java
                      ├── 📄 OrderResource.java
                      ├── 📄 OrderService.java
                      └── 📄 FeignAuthInterceptor.java
           └── 📁 resources/
               ├── 📄 application.yaml
               └── 📁 db/
                   └── 📁 migration/
                       ├── 📄 V2025.08.29.001__create_schema.sql
                       ├── 📄 V2025.08.29.002__create_table_order.sql
                       └── 📄 V2025.08.29.003__create_table_order_item.sql
    ├── 📄 pom.xml
    ├── 📄 Dockerfile
    └── 📄 Jenkinsfile
Source
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.5.5</version>
        <relativePath/>
    </parent>

    <groupId>store</groupId>
    <artifactId>order-service</artifactId>
    <version>1.0.0</version>

    <properties>
        <java.version>21</java.version>
        <spring-cloud.version>2025.0.0</spring-cloud.version>
        <maven.compiler.proc>full</maven.compiler.proc>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>order</artifactId>
            <version>${project.version}</version>
        </dependency>

        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>product</artifactId>
            <version>${project.version}</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.flywaydb</groupId>
            <artifactId>flyway-core</artifactId>
        </dependency>
        <dependency>
            <groupId>org.flywaydb</groupId>
            <artifactId>flyway-database-postgresql</artifactId>
        </dependency>


    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>
1
2
3
4
FROM openjdk:23-slim
VOLUME /tmp
COPY target/*.jar /app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
pipeline {
    agent any
    environment {
        SERVICE = 'order'
        NAME = "jpqv/${env.SERVICE}"
        AWS_REGION  = "us-east-2"
        EKS_CLUSTER = "eks-store"
    }
    stages {
        stage('Dependecies') {
            steps {
                build job: 'product', wait: true
                build job: 'order', wait: true
            }
        }
        stage('Build') { 
            steps {
                sh 'mvn -B -DskipTests clean package'
            }
        }      
        stage('Build & Push Image') {
            steps {
                withCredentials([usernamePassword(
                    credentialsId: 'dockerhub-credential',
                    usernameVariable: 'USERNAME',
                    passwordVariable: 'TOKEN')])
                {
                    sh "docker login -u $USERNAME -p $TOKEN"
                    sh "docker buildx create --use --platform=linux/arm64,linux/amd64 --node multi-platform-builder-${env.SERVICE} --name multi-platform-builder-${env.SERVICE}"
                    sh "docker buildx build --platform=linux/arm64,linux/amd64 --push --tag ${env.NAME}:latest --tag ${env.NAME}:${env.BUILD_ID} -f DockerFile ."
                    sh "docker buildx rm --force multi-platform-builder-${env.SERVICE}"
                }
            }
        }
        stage('Deploy to EKS') {
                steps {
                    withCredentials([[$class: 'AmazonWebServicesCredentialsBinding',
                    credentialsId: 'aws-credential',
                    accessKeyVariable: 'AWS_ACCESS_KEY_ID',
                    secretKeyVariable: 'AWS_SECRET_ACCESS_KEY']]) 
                {
                    sh """
                        # garante diretório padrão do kubeconfig
                        mkdir -p ~/.kube

                        # configura contexto do cluster no caminho padrão (~/.kube/config)
                        aws eks update-kubeconfig --region ${AWS_REGION} --name ${EKS_CLUSTER}

                        kubectl config current-context

                        # aplica manifest inicial se ainda não existir
                        if ! kubectl get deploy ${SERVICE} >/dev/null 2>&1; then
                        kubectl apply -f ./k8s/k8s.yaml
                        fi

                        # atualiza a imagem do Deployment
                        kubectl set image deploy/${SERVICE} ${SERVICE}=${NAME}:${BUILD_ID} --record

                        # espera o rollout
                        kubectl rollout status deployment/${SERVICE} --timeout=180s
                    """
                }
            }
        }
    }
}
apiVersion: apps/v1
kind: Deployment
metadata:
  name: order
spec:
  replicas: 1
  selector:
    matchLabels:
      app: order
  template:
    metadata:
      labels:
        app: order
    spec:
      containers:
        - name: order
          image: jpqv/order:latest
          imagePullPolicy: Always
          ports:
            - containerPort: 8080
          env:
            - name: POSTGRES_DB
              valueFrom:
                configMapKeyRef:
                  name: postgres-configmap
                  key: POSTGRES_DB
            - name: DATABASE_USERNAME
              valueFrom:
                secretKeyRef:
                  name: postgres-secrets
                  key: POSTGRES_USER
            - name: DATABASE_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: postgres-secrets
                  key: POSTGRES_PASSWORD
            - name: DATABASE_URL
              value: "jdbc:postgresql://postgres:5432/$(POSTGRES_DB)"
          resources:
            requests:
              memory: "200Mi"
              cpu: "50m"
            limits:
              memory: "300Mi"
              cpu: "200m"

---

apiVersion: v1
kind: Service
metadata:
  name: order
  labels:
    app: order
spec:
  type: ClusterIP
  ports:
    - port: 80
      protocol: TCP
      targetPort: 8080

  selector:
    app: order
server:
  port: 8080

spring:
  application:
    name: order

  mvc:
    problemdetails:
      enabled: true

  datasource:
    url: ${DATABASE_URL}
    username: ${DATABASE_USERNAME}
    password: ${DATABASE_PASSWORD}
    driver-class-name: org.postgresql.Driver

  flyway:
    baseline-on-migrate: true
    schemas: orders
  jpa:
    properties:
      hibernate:
        default_schema: orders

logging:
  level:
    store: debug
package store.order;

import java.time.OffsetDateTime;
import java.util.List;
import lombok.Builder;
import lombok.Data;
import lombok.experimental.Accessors;

@Data @Builder @Accessors(fluent = true, chain = true)
public class Order {
  String id;
  String accountId;
  OffsetDateTime date;
  List<OrderItem> items;
  Double total;
}
package store.order;

import lombok.Builder;
import lombok.Data;
import lombok.experimental.Accessors;

@Data @Builder @Accessors(fluent = true, chain = true)
public class OrderItem {
  String id;
  String productId;
  Integer quantity;
  Double total;
}
package store.order;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@EnableFeignClients(basePackages = "store.product")
@SpringBootApplication
public class OrderApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }

}
package store.order;

import java.util.ArrayList;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;

import store.product.ProductController;
import store.product.ProductOut;

@Service
public class OrderService {

  @Autowired
  private OrderRepository orderRepository;

  @Autowired
  private ProductController productController;

  @Transactional
  public OrderOut create(OrderIn in, String idAccount) {
    if (in == null || in.items() == null || in.items().isEmpty()) {
      throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Items are mandatory");
    }

    List<ProductOut> products = new ArrayList<>();
    in.items().forEach(it -> {
      ProductOut p = productController.findById(it.idProduct()).getBody();
      if (p == null) {
        throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Product not found: " + it.idProduct());
      }
      products.add(p);
    });

    OrderModel om = OrderParser.toModel(in, idAccount);

    double orderTotal = 0.0;
    for (int i = 0; i < in.items().size(); i++) {
      var inItem = in.items().get(i);
      var product = products.get(i);

      double total = product.price() * inItem.quantity();

      OrderItemModel im = new OrderItemModel();
      im.setOrder(om);
      im.setIdProduct(inItem.idProduct());
      im.setQuantity(inItem.quantity());
      im.setTotal(total);

      om.getItems().add(im);
      orderTotal += total;
    }
    om.setTotal(orderTotal);

    OrderModel saved = orderRepository.save(om);
    return OrderParser.toOut(saved, products);
  }

  public List<OrderOut> findAll(String idAccount) {
    var list = orderRepository.findAllByIdAccount(idAccount);
    // Para listar rápido, não precisamos montar os produtos: retornamos sem detalhes de produto
    return list.stream()
      .map(om -> OrderOut.builder()
        .id(om.getId())
        .date(om.getDate().toString())
        .items(null) // lista resumida
        .total(om.getTotal())
        .build())
      .toList();
  }

  public OrderOut findById(String id, String idAccount) {
    OrderModel om = orderRepository.findByIdAndIdAccount(id, idAccount)
      .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Order not found"));

    // carrega os produtos dos itens desse pedido
    List<ProductOut> products = om.getItems().stream()
        .map(it -> productController.findById(it.getIdProduct()).getBody())
        .toList();

    return OrderParser.toOut(om, products);
  }
}
package store.order;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

@RestController
public class OrderResource implements OrderController {

  @Autowired
  private OrderService orderService;

  @Override
  public ResponseEntity<OrderOut> create(OrderIn in) {
    String idAccount = CurrentRequest.idAccount();
    OrderOut out = orderService.create(in, idAccount);
    return ResponseEntity.created(
        ServletUriComponentsBuilder.fromCurrentRequest()
            .path("/{id}")
            .buildAndExpand(out.id())
            .toUri()
    ).body(out);
  }

  @Override
  public ResponseEntity<List<OrderOut>> findAll() {
    String idAccount = CurrentRequest.idAccount();
    return ResponseEntity.ok(orderService.findAll(idAccount));
  }

  @Override
  public ResponseEntity<OrderOut> findById(String id) {
    String idAccount = CurrentRequest.idAccount();
    return ResponseEntity.ok(orderService.findById(id, idAccount));
  }
}

``` { .java .copy .select linenums="1" } package store.order;

import java.util.List; import java.util.Optional; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Repository;

@Repository public interface OrderRepository extends CrudRepository {

List findAllByIdAccount(String idAccount);

Optional findByIdAndIdAccount(String id, String idAccount); }

package store.order;

import feign.RequestInterceptor;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

@Component
public class FeignAuthInterceptor {

  @Bean
  public RequestInterceptor forwardAuthHeader() {
    return template -> {
      var attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
      if (attrs == null) return;
      HttpServletRequest req = attrs.getRequest();
      String auth = req.getHeader("Authorization");
      if (auth != null && !auth.isBlank()) {
        template.header("Authorization", auth);
      }
    };
  }
}

``` { .sql .copy .select linenums="1" } CREATE SCHEMA IF NOT EXISTS orders;

1
2
3
4
5
6
CREATE TABLE orders (
  id           varchar(36) PRIMARY KEY,
  date         timestamptz NOT NULL,
  id_account   varchar(36) NOT NULL,
  total        decimal(12,2) NOT NULL
);
1
2
3
4
5
6
7
CREATE TABLE order_item (
  id          varchar(36) PRIMARY KEY,
  id_order    varchar(36) NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
  id_product  varchar(36) NOT NULL,
  quantity    integer NOT NULL,
  total       decimal(12,2) NOT NULL
);
mvn clean package spring-boot:run