跪拜 Guibai
← Back to the summary

10 Backend Requirements That Turned Simple Promises Into System-Wide Re-architectures

After 5 Years in Backend, I’ve Compiled 10 of the Most Absurd Requirements That Made Me Question My Life

Participating in the Absurd Requirements Awards topic essay

After 5 years of backend development, I’ve seen the moon at 4 a.m., and I’ve also seen requirement documents more absurd than the moon.

Sometimes I really wonder if product managers are secretly testing the limits of our patience. Today, let’s take stock of those requirements that made me question my life right at my desk. See how many you can get through without flipping the table.


1. "This API needs to support both pagination and non-pagination, but only one codebase"

The product manager’s original words were: "Sometimes the frontend needs pagination, sometimes it wants all the data. Can you write one API and control it with a parameter?"

I thought, isn’t this just adding a pageSize parameter? Simple.

Then he added: "But when it’s not paginated, the data format needs to be different from the paginated one, and it has to be compatible with the old version."

My expression at the time: 🤯

// And so this kind of "elegant" code was born
@GetMapping("/api/orders")
public ResponseEntity<?> getOrders(
    @RequestParam(required = false) Integer page,
    @RequestParam(required = false) Integer pageSize) {
    
    if (page != null && pageSize != null) {
        // Pagination mode
        Page<Order> orderPage = orderService.findAll(PageRequest.of(page - 1, pageSize));
        return ResponseEntity.ok(Map.of(
            "code", 200,
            "data", Map.of(
                "list", orderPage.getContent(),
                "total", orderPage.getTotalElements(),
                "page", page,
                "pageSize", pageSize
            ),
            "message", "success"
        ));
    } else {
        // Non-pagination mode - old version returns a completely different format
        List<Order> allOrders = orderService.findAll();
        return ResponseEntity.ok(Map.of(
            "success", true,
            "result", allOrders,
            "msg", "查询成功"
        ));
    }
}

Later, this API was deprecated after three versions because the product manager left, and the new product manager said: "Why make it so complicated? Just unify it with pagination, right?"

Me: 😶


2. "There needs to be a delete function, but the data can’t actually be deleted"

This was a real requirement I encountered at an e-commerce company.

Product manager: "When a user clicks delete order, the order should disappear from the list, but the backend needs to be able to query it."

Me: "Isn’t that just a soft delete? Just add an is_deleted field."

Product manager: "Right, but the user should also be able to see it in their recycle bin, and it should be automatically cleared after 30 days, and when clearing, some orders need to be kept, and the retention rules are—"

I quickly interrupted: "Wait, let me grab a notebook."

The final requirement matrix was:

Order Type Visible to User After Deletion After 30 Days Special Rule
Regular Order ✅ Recycle Bin Permanently Delete None
Group Buy Order ✅ Recycle Bin Do Not Delete Refundable if group buy fails
Flash Sale Order ❌ Disappears Directly Permanently Delete Irrecoverable
Member Order ✅ Recycle Bin Permanently Delete Retain for 90 days
-- Just the deletion logic alone became this mess
UPDATE orders 
SET 
    is_deleted = CASE 
        WHEN order_type = 'flash_sale' THEN 1  -- Flash sale: delete directly
        WHEN order_type = 'member' THEN 2       -- Member: mark for cleanup
        ELSE 1 
    END,
    deleted_at = CASE 
        WHEN order_type = 'flash_sale' THEN NOW()
        WHEN order_type = 'group_buy' THEN NULL -- Group buy: don't set deletion time
        ELSE NOW()
    END,
    recycle_days = CASE 
        WHEN order_type = 'member' THEN 90
        WHEN order_type = 'group_buy' THEN 0
        ELSE 30
    END
WHERE order_id = #{orderId};

Later, the ops team told me that the scheduled cleanup cron job ran for half a year and never succeeded once, because every time it tried to clean up, the database table locked up.


3. "This export feature needs to support Excel, but the data volume might be 1 million rows"

Product manager: "The client says they need to export order data."

Me: "Okay, export to Excel, no problem."

Product manager: "Well, the client says they sometimes have over 1 million orders a year and want to export all of them."

Me: "…You said Excel, right? The maximum number of rows in Excel is 1,048,576."

Product manager: "Right, so 1 million rows is just right, isn’t it?"

I said it’s not about the row count, but—a 1-million-row Excel file is about 200MB. Are you sure the client can open it?

The final compromise was: If it exceeds 100,000 rows, use asynchronous export, generate multiple files, pack them into a zip, and email the download link.

@Service
public class ExportService {
    
    private static final int MAX_ROWS_PER_FILE = 100_000;
    private static final int SYNC_THRESHOLD = 10_000;
    
    public void exportOrders(ExportRequest request) {
        long totalCount = orderRepository.count(request.getFilter());
        
        if (totalCount <= SYNC_THRESHOLD) {
            // Synchronous export, return directly
            byte[] excel = generateExcel(request, 0, (int) totalCount);
            saveAndNotify(request.getUserId(), excel);
        } else {
            // Asynchronous export, split into multiple files
            int fileCount = (int) Math.ceil((double) totalCount / MAX_ROWS_PER_FILE);
            asyncExport(request, fileCount, totalCount);
        }
    }
    
    private void asyncExport(ExportRequest request, int fileCount, long totalCount) {
        // Create export task
        ExportTask task = exportTaskRepository.save(ExportTask.builder()
            .userId(request.getUserId())
            .status("PROCESSING")
            .totalFiles(fileCount)
            .progress(0)
            .build());
        
        // Process in batches
        for (int i = 0; i < fileCount; i++) {
            int offset = i * MAX_ROWS_PER_FILE;
            byte[] part = generateExcel(request, offset, MAX_ROWS_PER_FILE);
            // Upload to OSS
            String url = ossService.upload(part, "export/" + task.getId() + "/part_" + i + ".xlsx");
            task.addFileUrl(url);
            task.setProgress((i + 1) * 100 / fileCount);
        }
        
        // Package into zip
        byte[] zip = zipService.compress(task.getFileUrls());
        String downloadUrl = ossService.upload(zip, "export/" + task.getId() + "/all.zip");
        
        // Send email notification
        emailService.send(request.getUserId(), "Export Complete", "Download link: " + downloadUrl);
    }
}

Later, the client said: "Actually, we only need the data from the last 3 months. We were just throwing out a number before."


4. "API response time must not exceed 200ms, but the data comes from three microservices"

This is the most classic one.

Product manager came to me with a competitive analysis report: "Look, this competitor’s page loads in just 200ms. Why can’t ours?"

I opened DevTools and saw that this page’s backend calls three microservices: user service, order service, and product service.

"These three services need to be called serially. User service 100ms, order service 150ms, product service 80ms. That adds up to 330ms."

Product manager: "Can you call them in parallel?"

Me: "…Yes, but it’s a significant amount of rework."

Product manager: "It can’t exceed 200ms. Figure it out."

Fine, I’ll change it.

// Before: Serial calls, 330ms
public PageData getPageData(Long userId) {
    UserInfo user = userService.getUser(userId);        // 100ms
    List<Order> orders = orderService.getOrders(userId); // 150ms
    List<Product> products = productService.getHot();    // 80ms
    return new PageData(user, orders, products);         // Total 330ms
}

// After: Parallel calls + caching
public PageData getPageData(Long userId) {
    CompletableFuture<UserInfo> userFuture = 
        CompletableFuture.supplyAsync(() -> 
            userCacheService.getOrLoad(userId));          // Cache hit: 5ms
    
    CompletableFuture<List<Order>> orderFuture = 
        CompletableFuture.supplyAsync(() -> 
            orderService.getRecentOrders(userId, 10));    // Limit 10: 50ms
    
    CompletableFuture<List<Product>> productFuture = 
        CompletableFuture.supplyAsync(() -> 
            productCacheService.getHotProducts());        // Local cache: 2ms
    
    CompletableFuture.allOf(userFuture, orderFuture, productFuture).join();
    
    // Total about 50ms, meets the requirement!
    return new PageData(userFuture.get(), orderFuture.get(), productFuture.get());
}

After going live, the product manager was very satisfied: "See, I told you it could be done within 200ms."

I pointed at the cache hit rate curve on the monitoring dashboard: "See this 99% hit rate? It’s supported by a Redis cluster and a two-tier local cache. The machine cost went up by 30%."

Product manager: "That’s fine, I’m not the one paying anyway."


5. "This API needs to support both GET and POST, and the parameter names are different"

Don’t laugh, this really happened.

A frontend colleague left, and the handover document said to call a certain API using a GET request. The new frontend colleague who took over found that the code was already using POST.

Then the product manager’s solution was: "Can’t your API just support both GET and POST?"

// The "simple compatibility" in the product manager’s eyes
@RequestMapping(value = "/api/user/profile", method = {RequestMethod.GET, RequestMethod.POST})
public Result getUserProfile(
    @RequestParam(required = false) String userId,     // For GET
    @RequestBody(required = false) UserProfileRequest request  // For POST
) {
    String uid;
    if (request != null && request.getUserId() != null) {
        uid = request.getUserId();  // POST: from body
    } else if (userId != null) {
        uid = userId;               // GET: from query parameter
    } else {
        throw new BadRequestException("userId is required");
    }
    return Result.success(userService.getProfile(uid));
}

Later one day, the frontend colleague asked in the group: "Why does the userId sometimes not get picked up on POST requests for this API?"

I checked the logs and saw he put the parameter in @RequestParam, but the Content-Type was application/json.

The backend took the blame for this one.


6. "Logs need to print all request parameters, but sensitive information must not be printed"

A requirement from the data security department, perfectly reasonable. The problem was—

"Which fields are sensitive information?"

"Phone number, ID card, bank card number, password, address, real name, email…"

"So basically all fields are sensitive information?"

"Not exactly, you can mask them. Asterisk out the middle four digits of the phone number, asterisk out the middle digits of the ID card…"

So I wrote a masking utility class and integrated it with over twenty APIs.

public class SensitiveDataMasker {
    
    private static final Set<String> SENSITIVE_FIELDS = Set.of(
        "phone", "mobile", "idCard", "bankCard", "password", 
        "realName", "email", "address", "idNumber"
    );
    
    public static String mask(String fieldName, String value) {
        if (value == null || value.isEmpty()) return value;
        
        return switch (fieldName) {
            case "phone", "mobile" -> 
                value.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
            case "idCard", "idNumber" -> 
                value.replaceAll("(\\d{4})\\d{10}(\\d{4})", "$1**********$2");
            case "bankCard" -> 
                value.replaceAll("(\\d{4})\\d+(\\d{4})", "$1****$2");
            case "email" -> 
                value.replaceAll("(.)(.*)(@.*)", "$1***$3");
            case "password" -> "******";
            default -> "***";
        };
    }
}

Then one day, QA filed a bug: "The user’s name on the order detail page shows three asterisks. The user complained."

I checked and found the realName field mapping was wrong. The masking rule turned "张三" into "***".

But the user said: "My name is Zhang San, not three stars!"


7. "This requirement is very simple, just add a button"

The nightmare phrase for all developers: "This requirement is very simple."

The product manager pointed at the prototype: "Add a button here. When clicked, pop up a confirmation box. After confirming, call the backend API, then refresh the page."

Me: "Hmm, that does sound simple."

Product manager: "Right, but—"

I knew there was a "but."

"—but this button should be displayed within 7 days of the user’s first login, and the user must be a VIP, and the user’s order count must exceed 3, and the user’s account balance must be greater than 100, and the user must not have clicked 'Don’t remind again,' and the button text should dynamically change based on the user’s gender. Female users see 'Claim Exclusive Offer,' male users see 'View Benefits,' and the popup style after clicking must match the brand color, and the popup must display the user’s currently available coupons, and the coupons should be sorted by expiration date, and expired ones should be displayed in gray but not hidden, and—"

"STOP!!!" I interrupted him. "You call this 'adding a button'?"

// This is the logic behind "adding a button"
public ButtonConfig getButtonConfig(Long userId) {
    User user = userService.getById(userId);
    LocalDate now = LocalDate.now();
    
    // Within 7 days of first login
    if (user.getFirstLoginDate().plusDays(7).isBefore(now)) {
        return ButtonConfig.hidden();
    }
    
    // Must be VIP
    if (!user.isVip()) {
        return ButtonConfig.hidden();
    }
    
    // Order count exceeds 3
    if (orderService.countByUserId(userId) <= 3) {
        return ButtonConfig.hidden();
    }
    
    // Account balance greater than 100
    if (user.getBalance().compareTo(new BigDecimal("100")) <= 0) {
        return ButtonConfig.hidden();
    }
    
    // Has not clicked "Don’t remind again"
    if (userPreferenceService.hasDismissed(userId, "VIP_BUTTON")) {
        return ButtonConfig.hidden();
    }
    
    // Dynamic text
    String text = "女".equals(user.getGender()) 
        ? "Claim Exclusive Offer" 
        : "View Benefits";
    
    return ButtonConfig.builder()
        .visible(true)
        .text(text)
        .coupons(couponService.getAvailableCoupons(userId))
        .build();
}

For this "simple" requirement, I wrote over 300 lines of code and submitted it for testing 3 times, because the product manager discovered new "buts" every time he tested it.


8. "The database query is too slow, can you optimize it?"

The DBA came to me: "This SQL runs for 3 seconds. Users are complaining."

SELECT * FROM orders 
WHERE status IN ('PAID', 'SHIPPED', 'DELIVERED') 
AND created_at BETWEEN '2025-01-01' AND '2025-12-31'
AND (user_id IN (SELECT id FROM users WHERE level >= 3) 
     OR total_amount > 1000)
ORDER BY created_at DESC;

Me: "This query is indeed slow. The main issues are the IN subquery and the OR condition."

DBA: "Can you optimize it to within 10ms?"

Me: "…A table with 50 million rows of data? 10ms? Are you serious?"

In the end, I made these optimizations:

-- 1. Add indexes
CREATE INDEX idx_orders_status_created ON orders(status, created_at);
CREATE INDEX idx_orders_user_amount ON orders(user_id, total_amount);

-- 2. Use UNION instead of OR (MySQL 5.7 doesn’t optimize OR well)
SELECT * FROM orders 
WHERE status IN ('PAID', 'SHIPPED', 'DELIVERED') 
AND created_at BETWEEN '2025-01-01' AND '2025-12-31'
AND user_id IN (SELECT id FROM users WHERE level >= 3)
UNION
SELECT * FROM orders 
WHERE status IN ('PAID', 'SHIPPED', 'DELIVERED') 
AND created_at BETWEEN '2025-01-01' AND '2025-12-31'
AND total_amount > 1000;

After optimization, it dropped from 3 seconds to 200ms. The DBA said: "Still not enough. The product team says it needs to be 10ms."

The final solution was to add Elasticsearch, sync the order data into ES, and route queries through ES. Response time dropped to 30ms.

But introducing ES brought data consistency issues, so we added Canal + MQ for data synchronization.

What started as a "slow query" optimization ended up becoming a microservice architecture upgrade project.


9. "This API needs to be exposed to third parties, but we don’t want them to see all the data"

This was a requirement for an open platform.

Product manager: "We need to open the order query API to partner merchants."

Me: "Okay, implement permission control. Merchants can only query their own orders."

Product manager: "Right, but the fields each merchant can see are different. Merchant A can see the price, Merchant B can’t see the price but can see the user’s phone number, Merchant C can’t see the price or the phone number but can see the order remarks…"

Me: "…How many merchants are there in total?"

Product manager: "Currently 20, but there will be more in the future."

I gritted my teeth and designed a field-level permission control:

@Service
public class FieldPermissionService {
    
    // Merchant-field permission mapping
    private static final Map<String, Set<String>> MERCHANT_FIELD_PERMISSIONS = Map.of(
        "MERCHANT_A", Set.of("orderId", "productName", "price", "status", "createdAt"),
        "MERCHANT_B", Set.of("orderId", "productName", "userPhone", "status", "createdAt"),
        "MERCHANT_C", Set.of("orderId", "productName", "remark", "status", "createdAt"),
        "MERCHANT_D", Set.of("orderId", "productName", "status", "createdAt")
        // ... 20 merchants
    );
    
    public Map<String, Object> filterFields(String merchantId, Order order) {
        Set<String> allowedFields = MERCHANT_FIELD_PERMISSIONS
            .getOrDefault(merchantId, Set.of("orderId", "productName", "status"));
        
        Map<String, Object> original = objectMapper.convertValue(order, Map.class);
        Map<String, Object> filtered = new HashMap<>();
        
        for (String field : allowedFields) {
            if (original.containsKey(field)) {
                filtered.put(field, original.get(field));
            }
        }
        return filtered;
    }
}

Later, the product manager said: "The field permissions have been changed to be configurable. Merchants can check off which fields are visible in the backend."

I silently changed the hardcoded Map to query a database, then added an approval workflow: merchant selects fields → platform reviews → takes effect.

What was originally a simple API exposure turned into a complete permission management system.


10. "Migrate this system from MySQL to MongoDB, but don’t take the service offline"

The final blow.

The CTO heard about MongoDB at a tech sharing session and thought the order data in our e-commerce system was very suitable for a document database.

CTO: "Order data is inherently a nested structure. MongoDB is more suitable than MySQL."

Me: "True, but the migration cost is high, and—"

CTO: "Complete it this quarter. No downtime."

Me: "…"

For two months, I implemented a dual-write solution:

┌─────────┐     ┌──────────┐     ┌─────────┐
│  App    │────▶│  Router  │────▶│  MySQL  │ (Primary)
└─────────┘     └──────────┘     └─────────┘
                      │
                      │ Async Dual-Write
                      ▼
                ┌──────────┐
                │  MongoDB │ (Shadow DB)
                └──────────┘
@Transactional
public Order createOrder(OrderRequest request) {
    // 1. Write to MySQL (primary)
    Order order = orderMapper.insert(request);
    
    // 2. Async write to MongoDB
    mongoTemplate.insertAsync(order);
    
    // 3. Record dual-write log
    syncLogService.record(order.getId(), "MYSQL", "SUCCESS");
    syncLogService.record(order.getId(), "MONGO", "PENDING");
    
    return order;
}

// Scheduled reconciliation task
@Scheduled(cron = "0 0/30 * * * ?")
public void reconciliation() {
    List<Long> diffIds = syncLogService.findDiff();
    for (Long id : diffIds) {
        Order mysqlOrder = orderMapper.findById(id);
        mongoTemplate.save(mysqlOrder); // Compensating write
        syncLogService.markSynced(id);
    }
}

After going live, everything was normal until one day, after a read API was switched to MongoDB, the response time went from unstable to timing out.

Investigation revealed: a MongoDB shard had failed, but because the dual-write solution didn’t have MongoDB failover, some requests timed out.

In the end, we urgently switched back to MySQL. The CTO said: "Let’s stick with MySQL for now. We’ll talk about MongoDB later."

Me: 😐


Final Words

After so many years in backend development, I’ve discovered a pattern: The simpler the requirement description, the more complex it is behind the scenes.

"Add a button" = Permission control + Dynamic text + Lifecycle management + User preferences + A/B testing

"Optimize it" = Index optimization + Cache design + Data synchronization + Architecture upgrade

"Make it compatible" = API version management + Data format conversion + Regression testing

But complaints aside, these "absurd requirements" have, to some extent, forced us to grow. Without that "simple" export requirement, I might never have learned asynchronous tasks and message queues. Without that "200ms" requirement, I wouldn’t have delved deeply into caching strategies.

Of course, it would be even better if product managers could think through the requirements clearly before proposing them. 🫠


What absurd requirements have you encountered that made you question your life? Let’s chat in the comments and see whose experience is more outrageous.

🏷️ This article participates in the Absurd Requirements Awards topic

Comments

Top 1 from juejin.cn, machine-translated. The original thread is authoritative.

Lsland

"Support both GET and POST on the same endpoint" is just a joke, right? Just write another endpoint and change it, problem solved.