Skip to main content
  1. Posts/

Hot Software Development Design Patterns in 2026

sun.ao
Author
sun.ao
I’m sun.ao, a programmer passionate about technology, focusing on AI and digital transformation.
Table of Contents

With the rise of cloud computing, AI, and distributed systems, traditional design patterns can no longer meet the demands of modern software development. This article introduces the hottest software development design patterns in 2026, with concise and easy-to-understand code examples.


1. Cloud Native & Microservices Patterns
#

1. Sidecar Pattern
#

Core Idea: Deploy auxiliary functions (logging, monitoring, configuration) as independent processes on the same host as the main application, sharing network and storage.

Use Cases: Log collection in Kubernetes, service mesh proxies.

// Main Application Service
public class MainApplication {
    public static void main(String[] args) {
        // Main business logic
        System.out.println("Main application started, listening on port 8080");
        // Sidecar accesses through shared network namespace
    }
}

// Sidecar Process (Independent Log Collector)
public class LogSidecar {
    public static void main(String[] args) {
        // Monitor main application's log files or ports
        System.out.println("Sidecar started, responsible for log collection and reporting");
        // Send logs to centralized logging system
    }
}

Kubernetes Deployment Example:

apiVersion: v1
kind: Pod
metadata:
  name: app-with-sidecar
spec:
  containers:
    - name: main-app
      image: my-app:latest
    - name: log-sidecar
      image: log-collector:latest  # Sidecar container

2. Circuit Breaker
#

Core Idea: When a downstream service fails, fail fast to prevent cascading crashes. Similar to an electrical fuse.

Use Cases: Microservice calls, external API calls.

// Implementation using Resilience4j
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;

public class PaymentService {
    private final CircuitBreaker circuitBreaker;

    public PaymentService() {
        CircuitBreakerConfig config = CircuitBreakerConfig.custom()
            .failureRateThreshold(50)           // Trigger circuit when failure rate exceeds 50%
            .waitDurationInOpenState(Duration.ofSeconds(30))  // Try recovery after 30 seconds
            .slidingWindowSize(10)              // Sliding window size
            .build();

        this.circuitBreaker = CircuitBreaker.of("payment", config);
    }

    public String processPayment(String orderId) {
        return circuitBreaker.executeSupplier(() -> {
            // Call downstream payment service
            return callPaymentGateway(orderId);
        });
    }

    // Fallback when circuit is open
    public String processPaymentFallback(String orderId, Exception e) {
        return "Payment service temporarily unavailable, please try again later";
    }
}

3. Saga Pattern
#

Core Idea: Break down distributed transactions into a series of local transactions, each with a corresponding compensation operation. Execute compensation to rollback on failure.

Use Cases: E-commerce order placement (inventory deduction → payment → shipping creation).

// Order Creation Saga
public class OrderSaga {
    private final InventoryService inventoryService;
    private final PaymentService paymentService;
    private final ShippingService shippingService;

    public void createOrder(Order order) {
        try {
            // Step 1: Deduct inventory
            inventoryService.deductStock(order.getProductId(), order.getQuantity());

            try {
                // Step 2: Process payment
                paymentService.charge(order.getUserId(), order.getAmount());

                try {
                    // Step 3: Create shipping order
                    shippingService.createShipment(order);
                    System.out.println("Order created successfully!");

                } catch (Exception e) {
                    // Compensate Step 2: Refund
                    paymentService.refund(order.getUserId(), order.getAmount());
                    throw e;
                }

            } catch (Exception e) {
                // Compensate Step 1: Restore inventory
                inventoryService.restoreStock(order.getProductId(), order.getQuantity());
                throw e;
            }

        } catch (Exception e) {
            System.out.println("Order creation failed: " + e.getMessage());
        }
    }
}

4. CQRS (Command Query Responsibility Segregation)
#

Core Idea: Use different models and databases for read and write operations, optimizing each for performance.

Use Cases: High-concurrency systems, complex query scenarios.

// Command Model (Write Operations)
public class OrderCommandService {
    private final OrderWriteRepository writeRepo;

    public void createOrder(CreateOrderCommand cmd) {
        Order order = new Order(cmd.getUserId(), cmd.getProducts());
        writeRepo.save(order);
        // Publish event to notify read model to update
        eventBus.publish(new OrderCreatedEvent(order));
    }

    public void updateOrderStatus(UpdateStatusCommand cmd) {
        Order order = writeRepo.findById(cmd.getOrderId());
        order.setStatus(cmd.getStatus());
        writeRepo.save(order);
        eventBus.publish(new OrderUpdatedEvent(order));
    }
}

// Query Model (Read Operations)
public class OrderQueryService {
    private final OrderReadRepository readRepo;  // Can be a different database

    public OrderDTO getOrder(Long orderId) {
        return readRepo.findById(orderId);
    }

    public List<OrderDTO> getOrdersByUser(Long userId) {
        return readRepo.findByUserId(userId);
    }
}

// Read model can be an optimized view
@Entity
@Table(name = "order_view")
public class OrderDTO {
    private Long id;
    private String userName;      // Denormalized, avoid joins
    private String productNames;  // Denormalized
    private String status;
    private LocalDateTime createdAt;
}

2. AI/LLM Application Patterns
#

1. RAG (Retrieval-Augmented Generation)
#

Core Idea: First retrieve relevant documents from a knowledge base, then pass them as context to the LLM to improve answer accuracy.

Use Cases: Enterprise knowledge base Q&A, intelligent customer service.

// Node.js + LangChain Implementation
import { OpenAI } from "openai";
import { PineconeClient } from "@pinecone-database/pinecone";

class RAGService {
    constructor() {
        this.openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
        this.pinecone = new PineconeClient();
    }

    async answer(question) {
        // 1. Convert question to embedding
        const questionEmbedding = await this.openai.embeddings.create({
            model: "text-embedding-3-small",
            input: question
        });

        // 2. Retrieve relevant documents from vector database
        const relevantDocs = await this.pinecone
            .index("knowledge-base")
            .query({
                vector: questionEmbedding.data[0].embedding,
                topK: 3
            });

        // 3. Build augmented prompt
        const context = relevantDocs.matches.map(m => m.metadata.text).join("\n\n");

        const prompt = `
Answer the question based on the following reference materials. If the information is not in the materials, please state so.

Reference Materials:
${context}

Question: ${question}

Answer:`;

        // 4. Call LLM to generate answer
        const response = await this.openai.chat.completions.create({
            model: "gpt-4",
            messages: [{ role: "user", content: prompt }]
        });

        return response.choices[0].message.content;
    }
}

2. Agent Pattern
#

Core Idea: AI agents can autonomously make decisions, call tools, and execute multi-step tasks.

Use Cases: Automated workflows, intelligent assistants.

// Simple AI Agent Implementation in Node.js
class AIAgent {
    constructor() {
        this.tools = {
            search: this.search.bind(this),
            calculate: this.calculate.bind(this),
            sendEmail: this.sendEmail.bind(this)
        };
    }

    async execute(task) {
        let step = 0;
        let context = { task, results: [] };

        while (step < 5) {  // Maximum 5 steps
            // Let LLM decide the next action
            const decision = await this.decideAction(context);

            if (decision.action === "finish") {
                return decision.result;
            }

            // Execute tool call
            const result = await this.tools[decision.action](decision.params);
            context.results.push({ action: decision.action, result });

            step++;
        }

        return "Task execution timeout";
    }

    async decideAction(context) {
        const prompt = `
Current task: ${context.task}
Executed steps: ${JSON.stringify(context.results)}

Available tools: search, calculate, sendEmail

Decide the next action, return JSON format:
{"action": "tool_name or finish", "params": {...}, "result": "result if finish"}
`;
        const response = await this.openai.chat.completions.create({
            model: "gpt-4",
            messages: [{ role: "user", content: prompt }],
            response_format: { type: "json_object" }
        });

        return JSON.parse(response.choices[0].message.content);
    }

    async search(query) {
        // Call search API
        return `Search results: Information about ${query}...`;
    }

    async calculate(expression) {
        return eval(expression);
    }

    async sendEmail(params) {
        console.log(`Sending email to ${params.to}: ${params.subject}`);
        return "Email sent successfully";
    }
}

// Usage Example
const agent = new AIAgent();
const result = await agent.execute("Check tomorrow's weather in Beijing and send an email to boss@company.com");

3. Prompt Chaining
#

Core Idea: Break down complex tasks into multiple LLM calls, where the output of one becomes the input of the next.

Use Cases: Content generation pipelines, code generation.

class PromptChain {
    async generateArticle(topic) {
        // Step 1: Generate outline
        const outline = await this.generateOutline(topic);

        // Step 2: Generate content for each section
        const sections = await this.generateSections(outline);

        // Step 3: Polish and optimize
        const article = await this.polishArticle(sections);

        // Step 4: Generate summary
        const summary = await this.generateSummary(article);

        return { article, summary };
    }

    async generateOutline(topic) {
        const response = await this.openai.chat.completions.create({
            model: "gpt-4",
            messages: [{
                role: "user",
                content: `Generate an outline for article "${topic}", return as JSON array`
            }]
        });
        return JSON.parse(response.choices[0].message.content);
    }

    async generateSections(outline) {
        const sections = [];
        for (const section of outline) {
            const response = await this.openai.chat.completions.create({
                model: "gpt-4",
                messages: [{
                    role: "user",
                    content: `Write content based on outline "${section.title}", key points: ${section.points.join(",")}`
                }]
            });
            sections.push({
                title: section.title,
                content: response.choices[0].message.content
            });
        }
        return sections;
    }

    async polishArticle(sections) {
        const content = sections.map(s => `## ${s.title}\n${s.content}`).join("\n\n");
        const response = await this.openai.chat.completions.create({
            model: "gpt-4",
            messages: [{
                role: "user",
                content: `Please polish the following article to make it more fluent:\n${content}`
            }]
        });
        return response.choices[0].message.content;
    }

    async generateSummary(article) {
        const response = await this.openai.chat.completions.create({
            model: "gpt-4",
            messages: [{
                role: "user",
                content: `Generate a 100-word summary for the following article:\n${article}`
            }]
        });
        return response.choices[0].message.content;
    }
}

3. Data Processing Patterns
#

1. Outbox Pattern
#

Core Idea: Ensure atomicity of database updates and message sending. Write messages to an Outbox table first, then send by an independent process.

Use Cases: Event notification between microservices.

// Order Service
@Service
public class OrderService {
    @Autowired
    private OrderRepository orderRepo;

    @Autowired
    private OutboxRepository outboxRepo;

    @Transactional
    public void createOrder(Order order) {
        // 1. Save order
        orderRepo.save(order);

        // 2. Write event to Outbox table (same transaction)
        OutboxEvent event = new OutboxEvent();
        event.setAggregateType("Order");
        event.setAggregateId(order.getId());
        event.setEventType("OrderCreated");
        event.setPayload(objectMapper.writeValueAsString(order));
        outboxRepo.save(event);
    }
}

// Outbox Table Structure
@Entity
@Table(name = "outbox")
public class OutboxEvent {
    @Id
    private UUID id;
    private String aggregateType;
    private String aggregateId;
    private String eventType;
    private String payload;
    private LocalDateTime createdAt;
}

// Independent Message Publisher
@Component
public class OutboxPublisher {
    @Scheduled(fixedRate = 1000)
    public void publishEvents() {
        List<OutboxEvent> events = outboxRepo.findTop100ByOrderByCreatedAtAsc();

        for (OutboxEvent event : events) {
            try {
                kafkaTemplate.send(event.getAggregateType(), event.getPayload());
                outboxRepo.delete(event);  // Delete after successful send
            } catch (Exception e) {
                log.error("Failed to send event: {}", event.getId());
            }
        }
    }
}

2. CDC (Change Data Capture)
#

Core Idea: Real-time capture of database changes to trigger downstream processing.

Use Cases: Data synchronization, real-time analytics, cache updates.

// Using Debezium to capture MySQL changes
@Component
public class OrderCDCProcessor {

    @KafkaListener(topics = "dbserver1.inventory.orders")
    public void processOrderChange(ChangeEvent<String, String> event) {
        JSONObject payload = new JSONObject(event.getValue());
        String operation = payload.getJSONObject("payload").getString("op");

        switch (operation) {
            case "c":  // Create
                handleOrderCreated(payload);
                break;
            case "u":  // Update
                handleOrderUpdated(payload);
                break;
            case "d":  // Delete
                handleOrderDeleted(payload);
                break;
        }
    }

    private void handleOrderCreated(JSONObject payload) {
        JSONObject after = payload.getJSONObject("payload").getJSONObject("after");
        Long orderId = after.getLong("id");

        // Update Elasticsearch index
        searchService.indexOrder(orderId);

        // Update Redis cache
        cacheService.cacheOrder(orderId, after.toString());

        // Send notification
        notificationService.notifyNewOrder(orderId);
    }
}

4. Reliability Patterns
#

1. Retry with Exponential Backoff
#

Core Idea: Retry after failure, but with exponentially increasing wait time to avoid avalanche effects.

public class RetryClient {
    private static final int MAX_RETRIES = 5;
    private static final long INITIAL_DELAY_MS = 100;

    public <T> T executeWithRetry(Supplier<T> operation) {
        int attempt = 0;
        long delay = INITIAL_DELAY_MS;

        while (attempt < MAX_RETRIES) {
            try {
                return operation.get();
            } catch (Exception e) {
                attempt++;
                if (attempt >= MAX_RETRIES) {
                    throw new RuntimeException("Retry attempts exhausted", e);
                }

                System.out.println("Retry attempt " + attempt + ", waiting " + delay + "ms");

                try {
                    Thread.sleep(delay);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                    throw new RuntimeException("Retry interrupted", ie);
                }

                delay *= 2;  // Exponential growth
                // Add random jitter to avoid multiple clients retrying simultaneously
                delay += (long) (delay * 0.1 * Math.random());
            }
        }

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

// Usage Example
RetryClient client = new RetryClient();
String result = client.executeWithRetry(() -> {
    return externalApi.call();  // Call that might fail
});

2. Bulkhead Pattern
#

Core Idea: Limit resource usage for each service to prevent one service from exhausting all resources.

// Implementation using Resilience4j
import io.github.resilience4j.bulkhead.Bulkhead;
import io.github.resilience4j.bulkhead.BulkheadConfig;

@Service
public class ExternalService {
    // Set up independent bulkheads for different services
    private final Bulkhead paymentBulkhead;
    private final Bulkhead notificationBulkhead;

    public ExternalService() {
        BulkheadConfig config = BulkheadConfig.custom()
            .maxConcurrentCalls(10)      // Maximum concurrent calls
            .maxWaitDuration(Duration.ofSeconds(1))
            .build();

        this.paymentBulkhead = Bulkhead.of("payment", config);
        this.notificationBulkhead = Bulkhead.of("notification", config);
    }

    public String callPayment(String orderId) {
        return paymentBulkhead.executeSupplier(() -> {
            return paymentClient.process(orderId);
        });
    }

    public void sendNotification(String message) {
        notificationBulkhead.executeRunnable(() -> {
            notificationClient.send(message);
        });
    }
}

5. Frontend Architecture Patterns
#

1. BFF (Backend for Frontend)
#

Core Idea: Provide customized backend APIs for different clients (Web, Mobile, Mini-programs).

// Node.js + Express Implementation

// Web BFF
const webBff = express();

webBff.get('/product/:id', async (req, res) => {
    const productId = req.params.id;

    // Aggregate multiple backend services
    const [product, reviews, recommendations] = await Promise.all([
        productService.getDetail(productId),
        reviewService.getReviews(productId, { limit: 10 }),
        recommendationService.getRelated(productId, { limit: 5 })
    ]);

    // Customized response format for Web
    res.json({
        product: {
            id: product.id,
            name: product.name,
            price: product.price,
            description: product.fullDescription,  // Web shows full description
            images: product.highResImages          // Web uses high-res images
        },
        reviews,
        recommendations
    });
});

// Mobile BFF
const mobileBff = express();

mobileBff.get('/product/:id', async (req, res) => {
    const productId = req.params.id;

    const product = await productService.getDetail(productId);

    // Customized response format for Mobile
    res.json({
        product: {
            id: product.id,
            name: product.name,
            price: product.price,
            description: product.shortDescription,  // Mobile shows short description
            thumbnail: product.thumbnailImage       // Mobile uses thumbnail
        }
        // Mobile doesn't return reviews and recommendations to reduce data
    });
});

2. Islands Architecture
#

Core Idea: Embed interactive “islands” in static HTML to reduce JavaScript bundle size.

// Astro Framework Example

// ---
// Most of the static page is pure HTML
// ---
import Header from '../components/Header.astro';
import Footer from '../components/Footer.astro';
import InteractiveCounter from '../components/Counter.jsx';  // Interactive island

const posts = await fetch('/api/posts').then(r => r.json());
---

<html>
<head>
    <title>Blog Home</title>
</head>
<body>
    <Header />  <!-- Static -->

    <main>
        <!-- Static content, zero JS -->
        <section class="posts">
            {posts.map(post => (
                <article>
                    <h2>{post.title}</h2>
                    <p>{post.excerpt}</p>
                </article>
            ))}
        </section>

        <!-- Interactive island, only this part loads JS -->
        <InteractiveCounter client:visible />
    </main>

    <Footer />  <!-- Static -->
</body>
</html>

// Counter.jsx - Only this component will load JavaScript
export default function Counter() {
    const [count, setCount] = useState(0);

    return (
        <div className="counter">
            <button onClick={() => setCount(c => c - 1)}>-</button>
            <span>{count}</span>
            <button onClick={() => setCount(c => c + 1)}>+</button>
        </div>
    );
}

6. Summary
#

Pattern CategoryCore PatternsProblems Solved
Cloud NativeSidecar, Circuit Breaker, Saga, CQRSService governance, fault tolerance, distributed transactions
AI/LLMRAG, Agent, Prompt ChainingKnowledge enhancement, autonomous decision-making, task decomposition
Data ProcessingOutbox, CDCData consistency, real-time synchronization
ReliabilityRetry, BulkheadFailure recovery, resource isolation
Frontend ArchitectureBFF, IslandsClient customization, performance optimization

When choosing design patterns, follow the simplicity first principle: start with the simplest solution, and introduce complex patterns only when bottlenecks are encountered. Over-engineering is often worse than no design at all.


References
#

Related articles