Skip to main content

Best Practices

This document introduces best practices for using Lovrabet Java OpenSDK in production environments, helping you build high-performance, highly available applications.


Table of Contents


1. Client Management

1.1 Use Singleton Pattern

LovrabetSDKClient is thread-safe. It's recommended to create a singleton and reuse it in your application to avoid frequent instance creation.

package com.example.config;

import com.lovrabet.runtime.opensdk.client.LovrabetSDKClient;

/**
* SDK Client Singleton Manager
*/
public class SdkClientManager {
private static volatile LovrabetSDKClient instance;

private SdkClientManager() {
// Private constructor to prevent external instantiation
}

public static LovrabetSDKClient getInstance() {
if (instance == null) {
synchronized (SdkClientManager.class) {
if (instance == null) {
String accessKey = ConfigManager.getAccessKey();
String baseUrl = ConfigManager.getBaseUrl();
instance = new LovrabetSDKClient(accessKey, baseUrl);
}
}
}
return instance;
}
}
// Not recommended: Creating new instance every call
public void someMethod() {
LovrabetSDKClient client = new LovrabetSDKClient(accessKey, baseUrl);
// ...
}

1.2 Configuration in Spring Boot Environment

Use Spring's dependency injection to manage SDK client:

@Configuration
public class LovrabetConfig {

@Value("${lovrabet.access-key}")
private String accessKey;

@Value("${lovrabet.base-url}")
private String baseUrl;

@Bean
public LovrabetSDKClient lovrabetSDKClient() {
return new LovrabetSDKClient(accessKey, baseUrl);
}
}
@Service
public class DataService {

private final LovrabetSDKClient sdkClient;

// Inject via constructor
public DataService(LovrabetSDKClient sdkClient) {
this.sdkClient = sdkClient;
}
}

2. Security

2.1 AccessKey Management

Method 1: Use Environment Variables

String accessKey = System.getenv("LOVRABET_ACCESS_KEY");
if (accessKey == null || accessKey.isEmpty()) {
throw new IllegalStateException("LOVRABET_ACCESS_KEY environment variable not set");
}
# Set in system environment
export LOVRABET_ACCESS_KEY="ak-your-access-key"

# Or pass when starting Java application
java -jar app.jar

Method 2: Configuration File + Config Center

# application.yml
lovrabet:
access-key: ${LOVRABET_ACCESS_KEY:}
base-url: https://runtime.lovrabet.com/openapi
@Value("${lovrabet.access-key}")
private String accessKey;

Method 3: Use Secret Management Service

// Use cloud provider's secret management service
String accessKey = secretManager.getSecret("lovrabet-access-key");
// ❌ Don't hardcode in source code
String accessKey = "ak-OD8xWhMOsL5ChQ3Akhv4uYYiu1fPOFQGVF9BULIeov8";

// ❌ Don't commit to code repository
// git add config.properties (containing plaintext accessKey)

// ❌ Don't print in logs
logger.info("AccessKey: " + accessKey); // Dangerous!

// ❌ Don't expose in exception messages
throw new Exception("AccessKey " + accessKey + " is invalid");

2.2 Data Transmission Security

// ✅ Always use HTTPS
String baseUrl = "https://runtime.lovrabet.com/openapi";

// ❌ Don't use HTTP
// String baseUrl = "http://runtime.lovrabet.com/openapi";

2.3 Sensitive Data Handling

// Mask sensitive information in logs
public void logCustomerInfo(Map<String, Object> customer) {
String phone = (String) customer.get("TEXT_2");
// Mask phone number: 138****1234
String maskedPhone = phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
logger.info("Customer phone: {}", maskedPhone);
}

3. Performance Optimization

3.1 Pagination Query Optimization

Choose Appropriate pageSize

// ✅ Recommended: Choose appropriate pageSize based on actual needs
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("pageSize", 50); // 20-100 is reasonable
paramMap.put("currentPage", 1);

// ❌ Not recommended: pageSize too small, causing too many requests
paramMap.put("pageSize", 5);

// ❌ Not recommended: pageSize too large, causing slow response and high memory usage
paramMap.put("pageSize", 10000);

Avoid Deep Pagination

// ❌ Avoid querying very deep page numbers
if (currentPage > 100) {
logger.warn("Deep pagination may affect performance: {}", currentPage);
// Consider using other methods like cursor-based pagination
}

3.2 Batch Operation Optimization

Use Thread Pool for Concurrent Processing

import java.util.concurrent.*;

/**
* Batch Create Data - Using Thread Pool
*/
public void batchCreate(List<Map<String, Object>> dataList) {
// Create fixed-size thread pool
ExecutorService executor = Executors.newFixedThreadPool(10);
List<Future<Boolean>> futures = new ArrayList<>();

for (Map<String, Object> data : dataList) {
Future<Boolean> future = executor.submit(() -> {
try {
LovrabetRequest request = buildRequest(data);
LovrabetResult<String> result = sdkClient.create(request);
return result.isSuccess();
} catch (Exception e) {
logger.error("Failed to create data", e);
return false;
}
});
futures.add(future);
}

// Wait for all tasks to complete
int successCount = 0;
for (Future<Boolean> future : futures) {
try {
if (future.get(30, TimeUnit.SECONDS)) {
successCount++;
}
} catch (Exception e) {
logger.error("Failed to get task result", e);
}
}

executor.shutdown();
logger.info("Batch creation completed, success: {}/{}", successCount, dataList.size());
}

Control Concurrency

// ✅ Recommended: Control concurrency to avoid server overload
ExecutorService executor = Executors.newFixedThreadPool(10); // Limit to 10 concurrent

// ❌ Not recommended: Unlimited concurrency may overload server
// ExecutorService executor = Executors.newCachedThreadPool();

3.3 Caching Strategy

For data that doesn't change often, use local caching to reduce API calls:

import com.github.benmanes.caffeine.cache.*;

/**
* Data Service with Caching
*/
public class CachedDataService {

private final LovrabetSDKClient sdkClient;

// Use Caffeine cache
private final Cache<String, Map<String, Object>> cache = Caffeine.newBuilder()
.maximumSize(1000) // Max 1000 entries
.expireAfterWrite(5, TimeUnit.MINUTES) // Expire after 5 minutes
.build();

public Map<String, Object> getData(String id) {
// Get from cache first
return cache.get(id, key -> {
// Cache miss, fetch from API
logger.debug("Cache miss, fetching from API: {}", id);
return fetchFromApi(id);
});
}

private Map<String, Object> fetchFromApi(String id) {
LovrabetRequest request = new LovrabetRequest();
request.setAppCode(appCode);
request.setModelCode(modelCode);
request.setId(Long.parseLong(id));

LovrabetResult<?> result = sdkClient.getOne(request);

if (result.isSuccess()) {
return (Map<String, Object>) result.getData();
}
return null;
}

// Clear cache when updating data
public void updateData(String id, Map<String, Object> data) {
// Update API
updateApi(id, data);

// Clear cache
cache.invalidate(id);
}
}

3.4 Connection Reuse

The SDK internally uses HTTP connection pooling, no additional configuration needed. But note:

// ✅ Recommended: Reuse SDK client instance
LovrabetSDKClient client = SdkClientManager.getInstance();

// ❌ Not recommended: Frequently creating and destroying clients
for (int i = 0; i < 1000; i++) {
LovrabetSDKClient client = new LovrabetSDKClient(accessKey, baseUrl);
// ...
}

4. Error Handling

4.1 Unified Error Handling

Create a unified error handling utility class:

package com.example.util;

import com.lovrabet.runtime.opensdk.model.LovrabetResult;

/**
* SDK Result Handling Utility
*/
public class ResultHandler {

/**
* Process result, throw exception on failure
*/
public static <T> T unwrap(LovrabetResult<T> result) {
if (result == null) {
throw new LovrabetException("API response is null");
}

if (result.isSuccess()) {
return result.getData();
} else {
throw new LovrabetException(
result.getResultCode(),
result.getResultMsg()
);
}
}

/**
* Process result, return default value on failure
*/
public static <T> T unwrapOrDefault(LovrabetResult<T> result, T defaultValue) {
if (result == null || !result.isSuccess()) {
return defaultValue;
}
return result.getData();
}

/**
* Process result, return Optional on failure
*/
public static <T> Optional<T> unwrapOptional(LovrabetResult<T> result) {
if (result != null && result.isSuccess()) {
return Optional.ofNullable(result.getData());
}
return Optional.empty();
}
}

/**
* Custom Exception Class
*/
public class LovrabetException extends RuntimeException {
private final String errorCode;

public LovrabetException(String message) {
super(message);
this.errorCode = "UNKNOWN";
}

public LovrabetException(String errorCode, String message) {
super(message);
this.errorCode = errorCode;
}

public String getErrorCode() {
return errorCode;
}
}

Usage example:

// Method 1: Throw exception on failure
try {
Map<String, Object> data = ResultHandler.unwrap(result);
// Process data
} catch (LovrabetException e) {
logger.error("API call failed [{}]: {}", e.getErrorCode(), e.getMessage());
}

// Method 2: Use default value on failure
List<Map<String, Object>> dataList =
ResultHandler.unwrapOrDefault(result, Collections.emptyList());

// Method 3: Use Optional
ResultHandler.unwrapOptional(result)
.ifPresent(data -> processData(data));

4.2 Retry Mechanism

For transient errors (like network jitter), implement automatic retry:

package com.example.util;

import java.util.function.Supplier;

/**
* Retry Utility Class
*/
public class RetryHelper {

/**
* Retry with exponential backoff
*/
public static <T> LovrabetResult<T> executeWithRetry(
Supplier<LovrabetResult<T>> operation,
int maxRetries,
long initialDelayMs) {

int attempt = 0;
long delay = initialDelayMs;

while (attempt <= maxRetries) {
try {
LovrabetResult<T> result = operation.get();

// Return on success
if (result != null && result.isSuccess()) {
if (attempt > 0) {
logger.info("Retry successful, attempts: {}", attempt + 1);
}
return result;
}

// Max retries reached
if (attempt == maxRetries) {
logger.error("Max retries {} reached, operation failed", maxRetries);
return result;
}

// Check if should retry
if (!shouldRetry(result)) {
logger.warn("Error not retryable, returning directly");
return result;
}

// Wait and retry
logger.warn("Operation failed, retrying in {} ms, attempt {}", delay, attempt + 2);
Thread.sleep(delay);

// Exponential backoff: double delay each retry
delay *= 2;
attempt++;

} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Retry interrupted", e);
} catch (Exception e) {
logger.error("Exception during operation", e);
if (attempt == maxRetries) {
throw new RuntimeException("Still failed after retries", e);
}
attempt++;
}
}

throw new RuntimeException("Should not reach here");
}

/**
* Check if error is retryable
*/
private static <T> boolean shouldRetry(LovrabetResult<T> result) {
if (result == null) {
return true; // Null result is retryable
}

String errorCode = result.getResultCode();

// These error codes should not retry
if ("INVALID_PARAM".equals(errorCode) ||
"ACCESS_DENIED".equals(errorCode) ||
"RESOURCE_NOT_FOUND".equals(errorCode)) {
return false;
}

// Other errors are retryable
return true;
}
}

Usage example:

// Max 3 retries, initial delay 1000ms, with exponential backoff
LovrabetResult<?> result = RetryHelper.executeWithRetry(
() -> sdkClient.getOne(request),
3, // Max 3 retries
1000L // Initial delay 1 second
);

4.3 Timeout Handling

For long-running operations, set timeout mechanism:

import java.util.concurrent.*;

/**
* Execute operation with timeout
*/
public <T> LovrabetResult<T> executeWithTimeout(
Supplier<LovrabetResult<T>> operation,
long timeoutSeconds) {

ExecutorService executor = Executors.newSingleThreadExecutor();

try {
Future<LovrabetResult<T>> future = executor.submit(operation::get);
return future.get(timeoutSeconds, TimeUnit.SECONDS);
} catch (TimeoutException e) {
logger.error("Operation timeout, exceeded {} seconds", timeoutSeconds);
throw new RuntimeException("Operation timeout", e);
} catch (Exception e) {
logger.error("Operation execution failed", e);
throw new RuntimeException("Operation failed", e);
} finally {
executor.shutdown();
}
}

5. Logging Management

5.1 Structured Logging

Use structured logging for easier analysis:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class DataService {
private static final Logger logger = LoggerFactory.getLogger(DataService.class);

public void createData(Map<String, Object> data) {
long startTime = System.currentTimeMillis();
String operation = "createData";

try {
logger.info("Starting operation - operation={}, dataSize={}",
operation, data.size());

LovrabetResult<String> result = sdkClient.create(request);
long duration = System.currentTimeMillis() - startTime;

if (result.isSuccess()) {
logger.info("Operation successful - operation={}, id={}, duration={}ms",
operation, result.getData(), duration);
} else {
logger.error("Operation failed - operation={}, errorCode={}, errorMsg={}, duration={}ms",
operation, result.getResultCode(), result.getResultMsg(), duration);
}

} catch (Exception e) {
long duration = System.currentTimeMillis() - startTime;
logger.error("Operation exception - operation={}, duration={}ms",
operation, duration, e);
throw e;
}
}
}

5.2 Log Levels

Use log levels appropriately:

// DEBUG: Detailed debug information
logger.debug("Request parameters: {}", paramMap);

// INFO: Important business flow information
logger.info("Starting customer data sync, count: {}", customers.size());

// WARN: Warning information, doesn't affect main flow
logger.warn("Data field {} is empty, using default value", fieldName);

// ERROR: Error information, needs attention
logger.error("Failed to create data: {}", result.getResultMsg());

// ERROR with exception: Exception stack trace
logger.error("Exception while processing data", e);

5.3 Sensitive Information Masking

/**
* Log Masking Utility
*/
public class LogMaskUtil {

/**
* Mask phone number
*/
public static String maskPhone(String phone) {
if (phone == null || phone.length() < 11) {
return phone;
}
return phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
}

/**
* Mask email
*/
public static String maskEmail(String email) {
if (email == null || !email.contains("@")) {
return email;
}
String[] parts = email.split("@");
String username = parts[0];
if (username.length() <= 2) {
return email;
}
String masked = username.substring(0, 2) + "***" +
username.substring(username.length() - 1);
return masked + "@" + parts[1];
}

/**
* Mask AccessKey
*/
public static String maskAccessKey(String accessKey) {
if (accessKey == null || accessKey.length() < 20) {
return "***";
}
return accessKey.substring(0, 10) + "***";
}
}

Usage example:

logger.info("Customer info - name: {}, phone: {}, email: {}",
customer.getName(),
LogMaskUtil.maskPhone(customer.getPhone()),
LogMaskUtil.maskEmail(customer.getEmail())
);

6. Testing Strategy

6.1 Unit Testing

Use Mock framework for unit testing:

import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

public class CustomerServiceTest {

@Test
public void testCreateCustomer() {
// Create Mock object
LovrabetSDKClient mockClient = Mockito.mock(LovrabetSDKClient.class);

// Set Mock behavior
LovrabetResult<String> mockResult = new LovrabetResult<>();
mockResult.setSuccess(true);
mockResult.setData("12345");

Mockito.when(mockClient.create(Mockito.any()))
.thenReturn(mockResult);

// Test
CustomerService service = new CustomerService(mockClient);
String id = service.createCustomer(buildTestCustomer());

// Verify
assertEquals("12345", id);
Mockito.verify(mockClient, Mockito.times(1)).create(Mockito.any());
}
}

6.2 Integration Testing

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class DataServiceIntegrationTest {

@Autowired
private DataService dataService;

@Test
public void testGetCustomers() {
List<Map<String, Object>> customers = dataService.getCustomers(1, 10);

assertNotNull(customers);
assertTrue(customers.size() > 0);
}

@Test
public void testCreateAndQuery() {
// Create test data
Map<String, Object> customerData = buildTestData();
String id = dataService.createCustomer(customerData);

assertNotNull(id);

// Query to verify
Map<String, Object> retrieved = dataService.getCustomer(id);
assertEquals(customerData.get("TEXT_1"), retrieved.get("TEXT_1"));
}
}

6.3 Test Data Isolation

/**
* Test Data Management
*/
public class TestDataManager {

private List<String> createdIds = new ArrayList<>();

/**
* Create test data
*/
public String createTestData(LovrabetSDKClient sdkClient) {
LovrabetRequest request = new LovrabetRequest();
// Set test data identifier
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("TEXT_1", "TEST_" + System.currentTimeMillis());
request.setParamMap(paramMap);

LovrabetResult<String> result = sdkClient.create(request);
if (result.isSuccess()) {
String id = result.getData();
createdIds.add(id); // Record created ID
return id;
}
return null;
}

/**
* Cleanup test data
*/
public void cleanup(LovrabetSDKClient sdkClient) {
for (String id : createdIds) {
// Cleanup test data logic
logger.debug("Cleaning up test data: {}", id);
}
createdIds.clear();
}
}

7. Deployment Recommendations

7.1 Environment Configuration

Use different configuration files for different environments:

# application-dev.yml (Development environment)
lovrabet:
access-key: ${LOVRABET_ACCESS_KEY}
base-url: https://runtime.lovrabet.com/openapi
app-code: app-dev-xxx

---
# application-prod.yml (Production environment)
lovrabet:
access-key: ${LOVRABET_ACCESS_KEY}
base-url: https://runtime.lovrabet.com/openapi
app-code: app-prod-xxx

7.2 Health Check

Implement health check endpoint:

@RestController
public class HealthController {

private final LovrabetSDKClient sdkClient;

@GetMapping("/health/lovrabet")
public Map<String, Object> checkLovrabetHealth() {
Map<String, Object> health = new HashMap<>();

try {
// Try calling a simple API
LovrabetRequest request = new LovrabetRequest();
request.setAppCode(appCode);

long startTime = System.currentTimeMillis();
LovrabetResult<?> result = sdkClient.getDatasetCodeList(request);
long duration = System.currentTimeMillis() - startTime;

health.put("status", result.isSuccess() ? "UP" : "DOWN");
health.put("responseTime", duration + "ms");
health.put("message", result.getResultMsg());

} catch (Exception e) {
health.put("status", "DOWN");
health.put("error", e.getMessage());
}

return health;
}
}

7.3 Monitoring Metrics

Use Micrometer to collect monitoring metrics:

import io.micrometer.core.instrument.*;

@Service
public class MonitoredDataService {

private final LovrabetSDKClient sdkClient;
private final MeterRegistry meterRegistry;
private final Counter successCounter;
private final Counter failureCounter;
private final Timer responseTimer;

public MonitoredDataService(
LovrabetSDKClient sdkClient,
MeterRegistry meterRegistry) {
this.sdkClient = sdkClient;
this.meterRegistry = meterRegistry;

// Register metrics
this.successCounter = Counter.builder("lovrabet.api.success")
.description("API call success count")
.register(meterRegistry);

this.failureCounter = Counter.builder("lovrabet.api.failure")
.description("API call failure count")
.register(meterRegistry);

this.responseTimer = Timer.builder("lovrabet.api.response.time")
.description("API response time")
.register(meterRegistry);
}

public Map<String, Object> getData(String id) {
return responseTimer.record(() -> {
LovrabetResult<?> result = sdkClient.getOne(buildRequest(id));

if (result.isSuccess()) {
successCounter.increment();
return (Map<String, Object>) result.getData();
} else {
failureCounter.increment();
return null;
}
});
}
}

7.4 Containerized Deployment

Dockerfile example:

FROM openjdk:11-jre-slim

# Set working directory
WORKDIR /app

# Copy jar file
COPY target/myapp.jar app.jar

# Set environment variables (production should use Secrets)
ENV LOVRABET_ACCESS_KEY=""

# Expose port
EXPOSE 8080

# Start application
ENTRYPOINT ["java", "-jar", "app.jar"]

Kubernetes deployment configuration:

apiVersion: v1
kind: Secret
metadata:
name: lovrabet-secret
type: Opaque
data:
access-key: <base64-encoded-access-key>

---
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 3
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: myapp:latest
env:
- name: LOVRABET_ACCESS_KEY
valueFrom:
secretKeyRef:
name: lovrabet-secret
key: access-key
ports:
- containerPort: 8080
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health/lovrabet
port: 8080
initialDelaySeconds: 20
periodSeconds: 5

Summary

By following these best practices, you can build high-performance, highly available, and secure production-grade applications:

  1. Client Management - Use singleton pattern to reuse client
  2. Security - Properly manage AccessKey, avoid hardcoding
  3. Performance Optimization - Reasonable pagination, use caching, control concurrency
  4. Error Handling - Unified error handling, implement retry mechanism
  5. Logging Management - Structured logging, mask sensitive information
  6. Testing Strategy - Comprehensive unit tests and integration tests
  7. Deployment Recommendations - Health checks, monitoring metrics, containerized deployment

Next Steps

After mastering best practices, it's recommended to continue learning in this order:

  1. 📖 Quick Start Complete your first CRUD program in 5 minutes

  2. 📚 Core Concepts Deep dive into how the SDK works

  3. 📋 API Reference Browse the complete interface documentation

  4. 💡 Business Examples Learn 5 real-world scenario implementations

  5. 🚀 Best Practices ← Current location Master production environment optimization techniques

  6. FAQ ← Recommended next step Resolve questions during usage


Need help? Visit FAQ or contact service@lovrabet.com