Backend API Design Best Practices
This article outlines comprehensive backend API design guidelines, covering parameter validation, extensibility, idempotency, logging, thread‑pool isolation, third‑party error handling, asynchronous processing, parallel queries, rate limiting, security, lock granularity, and strategies to avoid long‑running transactions.
Backend API Design
If you were asked to design an API, what issues would you consider?
1. Interface Parameter Validation
Both request parameters and return values must be validated.
Input validation: non‑null, length limits, format constraints such as email format.
Output handling: whether the response can be null, and if a default value should be returned (negotiated with the frontend).
2. Interface Extensibility
Consider whether a specific business flow should have a dedicated message‑push implementation or whether a generic push interface should be created for reuse across multiple flows. The strategy‑factory pattern can help select different implementations based on the scenario.
3. Interface Idempotency Design
Idempotency means that multiple calls to an interface do not change the business state; the result of repeated calls is the same as a single call.
Example: a user clicks the payment button multiple times due to network latency. Without idempotency, each click could trigger a separate payment.
Read‑only or delete operations are naturally idempotent and usually do not require extra handling.
Operations that modify state (e.g., transfers) must enforce idempotency, often by agreeing on a fixed token (generated per user ID, stored in Redis, and sent with each request).
4. Critical Interface Logging
Key business code should log input parameters, return values, and exception blocks to facilitate troubleshooting and responsibility tracing, especially in production where debugging is unavailable.
5. Core Interface Thread‑Pool Isolation
Interfaces such as classification queries or homepage data may use thread pools. If a regular interface exhausts the pool due to a bug, it can affect core business services; therefore, isolation is required.
6. Third‑Party Interface Exception Retry
When calling external services, consider:
Exception handling: decide whether to retry or treat the failure as final.
Request timeout: set a timeout to avoid hanging threads that can exhaust the pool.
Retry mechanism: define when and how many times to retry failed or timed‑out calls.
7. Asynchronous Processing
For actions like sending email or SMS after user registration, use asynchronous processing (e.g., a message queue) so that a notification failure does not cause the registration to fail.
8. Interface Query Optimization – Serial to Parallel
When building a homepage data API that needs user info, header info, news, etc., avoid serial calls; instead, perform parallel queries to improve performance.
Recommended: CompletableFuture ; not recommended: FutureTask .
Map<Long, List<SubjectLabelBO>> map = new HashMap<>();
List<CompletableFuture<Map<Long, List<SubjectLabelBO>>>> completableFutureList =
categoryBOList.stream().map(category ->
CompletableFuture.supplyAsync(() -> getLabelBOList(category), labelThreadPool)
).collect(Collectors.toList());
completableFutureList.forEach(future -> {
try {
Map<Long, List<SubjectLabelBO>> resultMap = future.get(); // blocks here
map.putAll(resultMap);
} catch (Exception e) {
e.printStackTrace();
}
});
public Map<Long, List<SubjectLabelBO>> getLabelBOList(SubjectCategoryBO category) {...}9. High‑Frequency Interface Rate Limiting
Implement custom annotation + AOP for rate limiting.
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimiter {
int value() default 1;
int durationInSeconds() default 1;
}
@Aspect
@Component
public class RateLimiterAspect {
private final ConcurrentHashMap<String, RateLimiter> rateLimiters = new ConcurrentHashMap<>();
@Pointcut("@annotation(RateLimiter)")
public void rateLimiterPointcut(RateLimiter rateLimiterAnnotation) {}
@Around("rateLimiterPointcut(rateLimiterAnnotation)")
public Object around(ProceedingJoinPoint joinPoint, RateLimiter rateLimiterAnnotation) throws Throwable {
int permits = rateLimiterAnnotation.value();
int durationInSeconds = rateLimiterAnnotation.durationInSeconds();
String key = joinPoint.getSignature().toLongString();
com.google.common.util.concurrent.RateLimiter rateLimiter = rateLimiters.computeIfAbsent(key,
k -> com.google.common.util.concurrent.RateLimiter.create((double) permits / durationInSeconds));
if (rateLimiter.tryAcquire()) {
return joinPoint.proceed();
} else {
throw new RuntimeException("Rate limit exceeded.");
}
}
}
@RestController
public class ApiController {
@GetMapping("/api/limited")
@RateLimiter(value = 10, durationInSeconds = 60) // 10 requests per minute
public String limitedEndpoint() {
return "This API has a rate limit of 10 requests per minute.";
}
@GetMapping("/api/unlimited")
public String unlimitedEndpoint() {
return "This API has no rate limit.";
}
}10. Interface Security
Configure black‑ and white‑lists using a Bloom filter. Detailed code is omitted; refer to Bloom filter documentation for implementation.
11. Interface Lock Granularity
In high‑concurrency scenarios, overly coarse locks can degrade performance. Use fine‑grained locking only on the critical shared resource.
Coarse lock example:
void test(){
synchronized (this) {
B();
A();
}
}Fine‑grained lock example:
void test(){
B();
synchronized (this) {
A();
}
}12. Avoid Long‑Transaction Issues
Long transactions can increase CPU/memory usage and slow down the entire service.
Root causes include problematic SQL and application‑level transaction control.
How to avoid long transactions:
Do not place RPC calls inside a transaction.
Execute read‑only queries outside the transaction when possible.
In concurrent scenarios, avoid @Transactional; use TransactionTemplate for fine‑grained control.
Example using @Transactional:
@Transactional
public int createUser(User user){
userDao.save(user);
passCertDao.updateFlag(user.getPassId());
// Remote RPC call
sendEmailRpc(user.getEmail());
return user.getUserId();
}Example using TransactionTemplate:
@Resource
private TransactionTemplate transactionTemplate;
public int createUser(User user){
transactionTemplate.execute(transactionStatus -> {
try {
userDao.save(user);
passCertDao.updateFlag(user.getPassId());
} catch (Exception e) {
transactionStatus.setRollbackOnly();
}
return true;
});
// Remote RPC call outside the transaction
sendEmailRpc(user.getEmail());
return user.getUserId();
}Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
