Java SDK

Spring Boot Integration

Production-ready Spring Boot 3.x integration with auto-configuration, validation, async processing, and webhook handling.

This guide uses the Sent Java SDK (dm.sent:sent-dm-java) with Spring Boot 3.x, Jakarta EE, and modern Java features.

Project Setup

Maven Dependencies

<dependencies>
    <dependency>
        <groupId>dm.sent</groupId>
        <artifactId>sent-dm-java</artifactId>
        <version>1.0.0</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springdoc</groupId>
        <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
        <version>2.3.0</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

Configuration

Application Properties

# application.yml
spring:
  application:
    name: sent-dm-integration
  profiles:
    active: ${SPRING_PROFILES_ACTIVE:dev}
  datasource:
    url: ${DATABASE_URL:jdbc:postgresql://localhost:5432/sent_dm}
    username: ${DATABASE_USERNAME:postgres}
    password: ${DATABASE_PASSWORD:password}
  jpa:
    hibernate:
      ddl-auto: validate
    show-sql: false
  flyway:
    enabled: true
    locations: classpath:db/migration

sent:
  api:
    key: ${SENT_DM_API_KEY}
    base-url: ${SENT_DM_BASE_URL:https://api.sent.dm}
    timeout-seconds: 30
  webhook:
    secret: ${SENT_DM_WEBHOOK_SECRET}
    path: /webhooks/sent
  async:
    core-pool-size: 5
    max-pool-size: 10
    queue-capacity: 100

server:
  port: 8080

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics
  endpoint:
    health:
      probes:
        enabled: true

springdoc:
  api-docs:
    path: /api-docs
  swagger-ui:
    path: /swagger-ui.html

---
# Profile: dev
spring:
  config:
    activate:
      on-profile: dev
  datasource:
    url: jdbc:h2:mem:sent_dm_dev
    driver-class-name: org.h2.Driver
  jpa:
    hibernate:
      ddl-auto: create-drop
    show-sql: true

---
# Profile: prod
spring:
  config:
    activate:
      on-profile: prod
  jpa:
    show-sql: false
logging:
  level:
    com.example.sent: WARN

DTOs

// dto/SendMessageRequest.java
package com.example.sent.dto;

import jakarta.validation.constraints.*;
import java.util.List;
import java.util.Map;

public record SendMessageRequest(
    @NotEmpty @Size(max = 100)
    List<@Pattern(regexp = "^\\+?[1-9]\\d{1,14}$") String> to,
    @NotBlank @Size(max = 255) String templateId,
    @NotBlank @Pattern(regexp = "^[a-zA-Z0-9_-]+$") String templateName,
    Map<String, String> parameters,
    List<@Pattern(regexp = "^(whatsapp|sms)$") String> channels
) {}

// dto/SendWelcomeRequest.java
package com.example.sent.dto;

import jakarta.validation.constraints.*;

public record SendWelcomeRequest(
    @NotBlank @Pattern(regexp = "^\\+?[1-9]\\d{1,14}$") String phoneNumber,
    @Size(max = 100) String name,
    @Pattern(regexp = "^[a-z]{2}$") String language
) {}

// dto/MessageResponse.java
package com.example.sent.dto;

import java.time.Instant;
import java.util.List;

public record MessageResponse(
    String messageId,
    String status,
    Instant timestamp,
    List<String> channels
) {}

// dto/webhook/WebhookEvent.java
package com.example.sent.dto.webhook;

import java.time.Instant;

public record WebhookEvent(String type, String id, Instant timestamp, WebhookEventData data) {}
public record WebhookEventData(String id, String status, WebhookError error) {}
public record WebhookError(String code, String message) {}

Entity Layer

// entity/WebhookEventEntity.java
package com.example.sent.entity;

import jakarta.persistence.*;
import org.hibernate.annotations.CreationTimestamp;
import java.time.Instant;

@Entity
@Table(name = "webhook_events", indexes = {
    @Index(name = "idx_event_id", columnList = "eventId"),
    @Index(name = "idx_status", columnList = "status")
})
public class WebhookEventEntity {
    @Id @GeneratedValue(strategy = GenerationType.UUID)
    private String id;
    @Column(nullable = false, unique = true)
    private String eventId;
    @Column(nullable = false)
    private String eventType;
    private String messageId;
    private String status;
    @Column(nullable = false, columnDefinition = "TEXT")
    private String payload;
    @Enumerated(EnumType.STRING)
    private ProcessingStatus processingStatus = ProcessingStatus.PENDING;
    private Integer retryCount = 0;
    @CreationTimestamp
    private Instant createdAt;

    public enum ProcessingStatus { PENDING, PROCESSING, COMPLETED, FAILED }

    public WebhookEventEntity() {}
    public WebhookEventEntity(String eventId, String eventType, String payload) {
        this.eventId = eventId;
        this.eventType = eventType;
        this.payload = payload;
    }
    // Getters and setters...
}

// repository/WebhookEventRepository.java
package com.example.sent.repository;

import com.example.sent.entity.WebhookEventEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;

@Repository
public interface WebhookEventRepository extends JpaRepository<WebhookEventEntity, String> {
    Optional<WebhookEventEntity> findByEventId(String eventId);
    boolean existsByEventId(String eventId);
}

Configuration Classes

// config/AsyncConfig.java
package com.example.sent.config;

import org.springframework.context.annotation.*;
import org.springframework.scheduling.annotation.*;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;

@Configuration
@EnableAsync
@EnableScheduling
public class AsyncConfig {
    @Bean(name = "webhookTaskExecutor")
    public Executor webhookTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("webhook-");
        executor.initialize();
        return executor;
    }
}

// config/SentConfig.java
package com.example.sent.config;

import dm.sent.client.SentDmClient;
import dm.sent.client.okhttp.SentDmOkHttpClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.*;
import java.time.Duration;

@Configuration
public class SentConfig {
    @Bean
    public SentDmClient sentClient(
            @Value("${sent.api.key}") String apiKey,
            @Value("${sent.api.base-url}") String baseUrl) {
        return SentDmOkHttpClient.builder()
            .apiKey(apiKey)
            .baseUrl(baseUrl)
            .timeout(Duration.ofSeconds(30))
            .build();
    }
}

Service Layer

// service/MessageService.java
package com.example.sent.service;

import com.example.sent.dto.*;
import java.util.concurrent.CompletableFuture;

public interface MessageService {
    MessageResponse sendMessage(SendMessageRequest request);
    MessageResponse sendWelcomeMessage(SendWelcomeRequest request);
    CompletableFuture<MessageResponse> sendMessageAsync(SendMessageRequest request);
}

// service/impl/MessageServiceImpl.java
package com.example.sent.service.impl;

import com.example.sent.dto.*;
import com.example.sent.service.MessageService;
import dm.sent.client.SentDmClient;
import dm.sent.core.JsonValue;
import dm.sent.models.messages.MessageSendParams;
import dm.sent.models.messages.MessageSendResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.time.Instant;
import java.util.List;
import java.util.concurrent.CompletableFuture;

@Service
public class MessageServiceImpl implements MessageService {
    private static final Logger logger = LoggerFactory.getLogger(MessageServiceImpl.class);
    private final SentDmClient sentClient;

    public MessageServiceImpl(SentDmClient sentClient) {
        this.sentClient = sentClient;
    }

    @Override
    public MessageResponse sendMessage(SendMessageRequest request) {
        logger.info("Sending message to {} recipients", request.to().size());
        MessageSendParams params = buildMessageParams(request);
        MessageSendResponse response = sentClient.messages().send(params);
        return new MessageResponse(response.id(), response.status(), Instant.now(),
            request.channels() != null ? request.channels() : List.of("whatsapp", "sms"));
    }

    @Override
    public MessageResponse sendWelcomeMessage(SendWelcomeRequest request) {
        String name = request.name() != null ? request.name() : "Customer";
        MessageSendParams params = MessageSendParams.builder()
            .addTo(request.phoneNumber())
            .addChannel("whatsapp")
            .template(MessageSendParams.Template.builder()
                .name("welcome")
                .parameters(MessageSendParams.Template.Parameters.builder()
                    .putAdditionalProperty("name", JsonValue.from(name))
                    .build())
                .build())
            .build();
        MessageSendResponse response = sentClient.messages().send(params);
        return new MessageResponse(response.id(), response.status(), Instant.now(), List.of("whatsapp"));
    }

    @Override
    @Async("webhookTaskExecutor")
    public CompletableFuture<MessageResponse> sendMessageAsync(SendMessageRequest request) {
        try {
            return CompletableFuture.completedFuture(sendMessage(request));
        } catch (Exception e) {
            CompletableFuture<MessageResponse> future = new CompletableFuture<>();
            future.completeExceptionally(e);
            return future;
        }
    }

    private MessageSendParams buildMessageParams(SendMessageRequest request) {
        var paramsBuilder = MessageSendParams.Template.Parameters.builder();
        if (request.parameters() != null) {
            request.parameters().forEach((k, v) ->
                paramsBuilder.putAdditionalProperty(k, JsonValue.from(v)));
        }
        var builder = MessageSendParams.builder()
            .to(request.to())
            .template(MessageSendParams.Template.builder()
                .id(request.templateId())
                .name(request.templateName())
                .parameters(paramsBuilder.build())
                .build());
        if (request.channels() != null) request.channels().forEach(builder::addChannel);
        return builder.build();
    }
}

// service/WebhookService.java
package com.example.sent.service;

import com.example.sent.dto.webhook.WebhookEvent;
import java.util.concurrent.CompletableFuture;

public interface WebhookService {
    CompletableFuture<Void> processWebhookAsync(String payload, String signature);
}

// service/impl/WebhookServiceImpl.java
package com.example.sent.service.impl;

import com.example.sent.dto.webhook.WebhookEvent;
import com.example.sent.entity.WebhookEventEntity;
import com.example.sent.repository.WebhookEventRepository;
import com.example.sent.service.WebhookService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Base64;
import java.util.concurrent.CompletableFuture;

@Service
public class WebhookServiceImpl implements WebhookService {
    private static final Logger logger = LoggerFactory.getLogger(WebhookServiceImpl.class);
    private final WebhookEventRepository repository;
    private final ObjectMapper mapper;
    private final String webhookSecret;

    public WebhookServiceImpl(WebhookEventRepository repository, ObjectMapper mapper,
            @Value("${sent.webhook.secret:}") String webhookSecret) {
        this.repository = repository;
        this.mapper = mapper;
        this.webhookSecret = webhookSecret;
    }

    @Override
    @Async("webhookTaskExecutor")
    public CompletableFuture<Void> processWebhookAsync(String payload, String signature) {
        try {
            if (!verifySignature(payload, signature)) {
                throw new SecurityException("Invalid webhook signature");
            }
            WebhookEvent event = mapper.readValue(payload, WebhookEvent.class);
            if (repository.existsByEventId(event.id())) {
                return CompletableFuture.completedFuture(null);
            }
            WebhookEventEntity entity = new WebhookEventEntity(event.id(), event.type(), payload);
            entity.setSignature(signature);
            if (event.data() != null) {
                entity.setMessageId(event.data().id());
                entity.setStatus(event.data().status());
            }
            repository.save(entity);
            processEvent(event, entity);
            return CompletableFuture.completedFuture(null);
        } catch (Exception e) {
            logger.error("Failed to process webhook", e);
            CompletableFuture<Void> future = new CompletableFuture<>();
            future.completeExceptionally(e);
            return future;
        }
    }

    private void processEvent(WebhookEvent event, WebhookEventEntity entity) {
        entity.setProcessingStatus(WebhookEventEntity.ProcessingStatus.PROCESSING);
        repository.save(entity);
        logger.info("Processing event: {} - type: {}", event.id(), event.type());
        // Handle specific event types here...
        entity.setProcessingStatus(WebhookEventEntity.ProcessingStatus.COMPLETED);
        entity.setCreatedAt(Instant.now()); // Use as processedAt
        repository.save(entity);
    }

    private boolean verifySignature(String payload, String signature) {
        if (webhookSecret == null || webhookSecret.isBlank()) return true;
        if (signature == null) return false;
        try {
            Mac mac = Mac.getInstance("HmacSHA256");
            mac.init(new SecretKeySpec(webhookSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
            byte[] hash = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
            String computed = Base64.getEncoder().encodeToString(hash);
            return signature.equals(computed) || signature.equals("sha256=" + computed);
        } catch (Exception e) {
            return false;
        }
    }
}

REST Controllers

// controller/MessageController.java
package com.example.sent.controller;

import com.example.sent.dto.*;
import com.example.sent.service.MessageService;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.concurrent.CompletableFuture;

@Tag(name = "Messages")
@RestController
@RequestMapping("/api/v1/messages")
public class MessageController {
    private final MessageService messageService;

    public MessageController(MessageService messageService) {
        this.messageService = messageService;
    }

    @PostMapping("/send")
    public ResponseEntity<MessageResponse> sendMessage(@Valid @RequestBody SendMessageRequest request) {
        return ResponseEntity.ok(messageService.sendMessage(request));
    }

    @PostMapping("/welcome")
    public ResponseEntity<MessageResponse> sendWelcome(@Valid @RequestBody SendWelcomeRequest request) {
        return ResponseEntity.ok(messageService.sendWelcomeMessage(request));
    }

    @PostMapping("/send/async")
    public CompletableFuture<ResponseEntity<MessageResponse>> sendAsync(@Valid @RequestBody SendMessageRequest request) {
        return messageService.sendMessageAsync(request).thenApply(ResponseEntity::ok);
    }
}

// controller/WebhookController.java
package com.example.sent.controller;

import com.example.sent.service.WebhookService;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
import java.util.concurrent.CompletableFuture;

@Tag(name = "Webhooks")
@RestController
@RequestMapping("/webhooks")
public class WebhookController {
    private final WebhookService webhookService;

    public WebhookController(WebhookService webhookService) {
        this.webhookService = webhookService;
    }

    @PostMapping("/sent")
    public CompletableFuture<ResponseEntity<Map<String, Object>>> handleWebhook(
            @RequestBody String payload,
            @RequestHeader(value = "X-Webhook-Signature", required = false) String signature) {
        return webhookService.processWebhookAsync(payload, signature)
            .thenApply(v -> ResponseEntity.ok(Map.of("received", true)))
            .exceptionally(ex -> ResponseEntity.status(500).body(Map.of("error", ex.getMessage())));
    }
}

Exception Handling

// exception/GlobalExceptionHandler.java
package com.example.sent.exception;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.ConstraintViolationException;
import org.springframework.http.*;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;
import java.net.URI;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;

@RestControllerAdvice
public class GlobalExceptionHandler {
    private static final String ERROR_PREFIX = "https://api.example.com/errors/";

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ProblemDetail> handleValidation(MethodArgumentNotValidException ex, HttpServletRequest req) {
        ProblemDetail problem = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST);
        problem.setType(URI.create(ERROR_PREFIX + "validation-error"));
        problem.setTitle("Validation Failed");
        problem.setInstance(URI.create(req.getRequestURI()));
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getAllErrors().forEach(e -> {
            String field = e instanceof FieldError ? ((FieldError) e).getField() : e.getObjectName();
            errors.put(field, e.getDefaultMessage());
        });
        problem.setProperty("errors", errors);
        return ResponseEntity.badRequest().body(problem);
    }

    @ExceptionHandler(ConstraintViolationException.class)
    public ResponseEntity<ProblemDetail> handleConstraint(ConstraintViolationException ex, HttpServletRequest req) {
        ProblemDetail problem = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST);
        problem.setType(URI.create(ERROR_PREFIX + "constraint-violation"));
        problem.setTitle("Constraint Violation");
        problem.setInstance(URI.create(req.getRequestURI()));
        return ResponseEntity.badRequest().body(problem);
    }

    @ExceptionHandler(SecurityException.class)
    public ResponseEntity<ProblemDetail> handleSecurity(SecurityException ex, HttpServletRequest req) {
        ProblemDetail problem = ProblemDetail.forStatus(HttpStatus.FORBIDDEN);
        problem.setType(URI.create(ERROR_PREFIX + "security-error"));
        problem.setTitle("Security Error");
        problem.setInstance(URI.create(req.getRequestURI()));
        return ResponseEntity.status(HttpStatus.FORBIDDEN).body(problem);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ProblemDetail> handleGeneric(Exception ex, HttpServletRequest req) {
        ProblemDetail problem = ProblemDetail.forStatus(HttpStatus.INTERNAL_SERVER_ERROR);
        problem.setType(URI.create(ERROR_PREFIX + "internal-error"));
        problem.setTitle("Internal Server Error");
        problem.setInstance(URI.create(req.getRequestURI()));
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(problem);
    }
}

Testing

// controller/MessageControllerTest.java
package com.example.sent.controller;

import com.example.sent.dto.*;
import com.example.sent.service.MessageService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;
import java.time.Instant;
import java.util.List;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@WebMvcTest(MessageController.class)
class MessageControllerTest {
    @Autowired private MockMvc mockMvc;
    @Autowired private ObjectMapper mapper;
    @MockitoBean private MessageService messageService;

    @Test
    void sendWelcome_ShouldReturn200() throws Exception {
        when(messageService.sendWelcomeMessage(any()))
            .thenReturn(new MessageResponse("msg_123", "pending", Instant.now(), List.of("whatsapp")));

        SendWelcomeRequest req = new SendWelcomeRequest("+1234567890", "John", "en");
        mockMvc.perform(post("/api/v1/messages/welcome")
                .contentType(MediaType.APPLICATION_JSON)
                .content(mapper.writeValueAsString(req)))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.messageId").value("msg_123"));
    }

    @Test
    void sendWelcome_WithInvalidPhone_ShouldReturn400() throws Exception {
        SendWelcomeRequest req = new SendWelcomeRequest("invalid", "John", null);
        mockMvc.perform(post("/api/v1/messages/welcome")
                .contentType(MediaType.APPLICATION_JSON)
                .content(mapper.writeValueAsString(req)))
            .andExpect(status().isBadRequest());
    }
    // Additional tests...
}

// service/impl/MessageServiceImplTest.java
package com.example.sent.service.impl;

import com.example.sent.dto.*;
import dm.sent.client.SentDmClient;
import dm.sent.models.messages.*;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.*;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class MessageServiceImplTest {
    @Mock private SentDmClient sentClient;
    @Mock private SentDmClient.MessageClient messageClient;
    @InjectMocks private MessageServiceImpl messageService;

    @Test
    void sendMessage_ShouldReturnResponse() {
        when(sentClient.messages()).thenReturn(messageClient);
        when(messageClient.send(any())).thenReturn(
            MessageSendResponse.builder().id("msg_123").status("pending").build());

        SendMessageRequest req = new SendMessageRequest(
            List.of("+1234567890"), "tpl-id", "welcome", null, List.of("whatsapp"));
        MessageResponse resp = messageService.sendMessage(req);

        assertThat(resp.messageId()).isEqualTo("msg_123");
    }
    // Additional tests...
}

Docker Compose

version: '3.8'

services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - SPRING_PROFILES_ACTIVE=dev
      - SENT_DM_API_KEY=${SENT_DM_API_KEY}
      - SENT_DM_WEBHOOK_SECRET=${SENT_DM_WEBHOOK_SECRET}
      - DATABASE_URL=jdbc:postgresql://postgres:5432/sent_dm
    depends_on:
      postgres:
        condition: service_healthy

  postgres:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: sent_dm
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5

volumes:
  postgres_data:

Dockerfile

FROM eclipse-temurin:21-jdk-alpine AS builder
WORKDIR /app
COPY .mvn/ .mvn
COPY mvnw pom.xml ./
RUN chmod +x mvnw && ./mvnw dependency:go-offline
COPY src ./src
RUN ./mvnw clean package -DskipTests

FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
RUN addgroup -S sent && adduser -S sent -G sent
COPY --from=builder /app/target/*.jar app.jar
RUN chown -R sent:sent /app
USER sent
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:8080/actuator/health || exit 1
ENTRYPOINT ["java", "-jar", "app.jar"]

Environment Variables

# .env
SENT_DM_API_KEY=your_api_key_here
SENT_DM_WEBHOOK_SECRET=your_webhook_secret
SENT_DM_BASE_URL=https://api.sent.dm
DATABASE_URL=jdbc:postgresql://localhost:5432/sent_dm
DATABASE_USERNAME=postgres
DATABASE_PASSWORD=password
SPRING_PROFILES_ACTIVE=dev

Project Structure

src/
├── main/java/com/example/sent/
│   ├── SentDmApplication.java
│   ├── config/
│   │   ├── AsyncConfig.java
│   │   └── SentConfig.java
│   ├── controller/
│   │   ├── MessageController.java
│   │   └── WebhookController.java
│   ├── dto/
│   │   ├── SendMessageRequest.java
│   │   ├── SendWelcomeRequest.java
│   │   ├── MessageResponse.java
│   │   └── webhook/WebhookEvent.java
│   ├── entity/WebhookEventEntity.java
│   ├── exception/GlobalExceptionHandler.java
│   ├── repository/WebhookEventRepository.java
│   └── service/
│       ├── MessageService.java
│       ├── WebhookService.java
│       └── impl/
│           ├── MessageServiceImpl.java
│           └── WebhookServiceImpl.java
├── main/resources/application.yml
└── test/java/com/example/sent/
    └── controller/MessageControllerTest.java

Next Steps

On this page