DEV Community

minho lee
minho lee

Posted on

From Sequential to Structural: A Paradigm Shift After 15 Years of Java Development

From Sequential to Structural: A Paradigm Shift After 15 Years of Java Development

For over a decade, I've been building enterprise systems with the Java + Spring + Oracle stack. Recently, while building an AI agent orchestration system, I found myself deeply reflecting on the development patterns I had unconsciously followed all these years.

When we first learn to program, we all start the same way. We write code line by line, assign variables, run loops, and use conditionals. This is the Sequential, Bottom-up style.

But while building a multi-agent system with Claude Code, I realized that a completely different mindset was necessary: Top-down, Structural programming.


The Sequential Mindset I Was Comfortable With

Here's typical code you'd write in a Spring service layer:

@Service
public class OrderService {

    public OrderResult processOrder(OrderRequest request) {
        // 1. Validate user
        User user = userService.validateUser(request.getUserId());

        // 2. Check inventory
        Inventory inventory = inventoryService.checkStock(request.getProductId());

        // 3. Process payment
        Payment payment = paymentService.process(user, request.getAmount());

        // 4. Create order
        Order order = orderRepository.save(new Order(user, inventory, payment));

        return new OrderResult(order);
    }
}
Enter fullscreen mode Exit fullscreen mode

Familiar, right? This is bottom-up thinking. Step 1 finishes, then step 2, then step 3... executed in order.

The problem arises when validateUser, checkStock, and process each involve external API or DB calls. They execute sequentially even though they have no dependencies on each other, wasting time.

Sequential vs Structural Service Comparison


Lessons First Learned from Oracle

I actually first encountered this concept through Oracle query optimization.

-- Sequential thinking: assuming subqueries execute in order
SELECT * FROM orders o
WHERE o.user_id IN (SELECT user_id FROM users WHERE status = 'ACTIVE')
  AND o.product_id IN (SELECT product_id FROM products WHERE category = 'ELECTRONICS');
Enter fullscreen mode Exit fullscreen mode

Initially, I thought "it'll query users first, then products." But when you look at the execution plan (EXPLAIN PLAN), it's completely different.

The Oracle optimizer analyzes the dependency graph to determine the optimal execution order. If two subqueries are independent, it runs them in parallel and reorders joins based on statistics.

Databases were already doing top-down structural execution.

Oracle Execution Plan


CompletableFuture: A Structural Approach in Java

Since Java 8, we can apply this pattern with CompletableFuture:

@Service
public class OrderServiceAsync {

    public CompletableFuture<OrderResult> processOrderAsync(OrderRequest request) {
        // Start independent tasks concurrently
        CompletableFuture<User> userFuture =
            CompletableFuture.supplyAsync(() -> userService.validateUser(request.getUserId()));

        CompletableFuture<Inventory> inventoryFuture =
            CompletableFuture.supplyAsync(() -> inventoryService.checkStock(request.getProductId()));

        // Process payment when both complete (depends on user and inventory)
        return userFuture.thenCombine(inventoryFuture, (user, inventory) -> {
            Payment payment = paymentService.process(user, request.getAmount());
            Order order = orderRepository.save(new Order(user, inventory, payment));
            return new OrderResult(order);
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

The code structure itself expresses the dependency tree:

CompletableFuture Dependency Tree

User and Inventory execute concurrently, and Payment is only processed after both complete. The correct order emerges without explicitly specifying it.


Spring WebFlux: More Natural Structural Programming

With Reactor-based WebFlux, this pattern becomes even more natural:

@Service
public class OrderServiceReactive {

    public Mono<OrderResult> processOrder(OrderRequest request) {
        Mono<User> user = userService.validateUser(request.getUserId());
        Mono<Inventory> inventory = inventoryService.checkStock(request.getProductId());

        // zip: combine when both Monos complete
        return Mono.zip(user, inventory)
            .flatMap(tuple -> {
                User u = tuple.getT1();
                Inventory inv = tuple.getT2();
                return paymentService.process(u, request.getAmount())
                    .map(payment -> new OrderResult(u, inv, payment));
            });
    }
}
Enter fullscreen mode Exit fullscreen mode

Mono.zip() is the key. It plays exactly the same role as JavaScript's Promise.all():

"Start all child nodes, wait for them together, then combine the results"


Recent Experience: AI Agent Orchestration

I felt this paradigm shift most acutely when building a multi-agent system with Claude Code.

Initially, I designed it sequentially:

1. Research Agent → Gather information
2. Analysis Agent → Analyze
3. Writing Agent → Write
4. Review Agent → Review
Enter fullscreen mode Exit fullscreen mode

But in reality, many tasks were parallelizable:

AI Agent Dependency Tree

Research A, Research B, and Image Agent have no dependencies on each other. Running them concurrently reduces total time to the critical path rather than the sum.


Memoization: The Essence of Caching Strategy

Have you used @Cacheable in Spring?

@Cacheable(value = "users", key = "#userId")
public User findUser(Long userId) {
    return userRepository.findById(userId).orElseThrow();
}
Enter fullscreen mode Exit fullscreen mode

This is memoization. It caches results of functions that return the same output for the same input.

Why memoization is essential in the top-down model: the same node can be referenced multiple times in a tree structure.

Why Memoization is Needed

Without caching, User(1) would query the DB twice. With O(1) cache lookup, it queries once and reuses.

Cache Concerns in LLM Agents

While designing agent systems recently, my biggest concern has been cache key design for LLM calls:

  • Exact same prompt? → Too strict
  • Semantic similarity? → Computational cost
  • Context hash? → What counts as context?

Achieving O(1) while maintaining meaningful cache hit rates is the challenge.


Event-Driven Systems: Spring Event and Kafka

Spring's @EventListener and Kafka consumers follow the same pattern:

@Component
public class OrderEventHandler {

    @EventListener
    public void handleOrderCreated(OrderCreatedEvent event) {
        // Executes when event occurs
    }

    @EventListener
    public void handlePaymentCompleted(PaymentCompletedEvent event) {
        // Executes when event occurs
    }
}
Enter fullscreen mode Exit fullscreen mode

Here, listeners are leaf nodes of the tree. They activate when events (button clicks, message arrivals) occur.

Spring Event-Driven Tree

You can't specify "order event first, then payment event." Events arrive when they arrive. What matters is only the dependency structure.


Philosophical Differences Summarized

Sequential, Bottom-up (Traditional Spring Service)

Aspect Description
Program Definition Sequence of method calls
Order Explicit and specified
Dependencies Implicit in code order
Intermediate Values Stored in variables (auto-cached)

Structural, Top-down (Reactive/Async)

Aspect Description
Program Definition Dependency graph
Order Emerges at runtime
Dependencies Explicit via zip, combine, thenCompose
Intermediate Values Requires memoization (@Cacheable)

A Developer's Journey

Looking back, my growth followed this path:

  1. Sequential Phase - Writing procedural code with for and if
  2. Structured Phase - Spring DI, layered architecture, design patterns
  3. Structural Phase - CompletableFuture, WebFlux, event-driven architecture

The third phase isn't optional. For complex problems like MSA, async systems, and AI agent orchestration, it's an inevitable evolution.

Java Developer's Journey


Conclusion: Practical Application

Top-down structural programming isn't an abstract concept. It's embedded in the tools we already use:

Tool/Technology Structural Element
Oracle Optimizer Query execution plan (dependency graph)
CompletableFuture thenCombine, allOf
Spring WebFlux Mono.zip, Flux.merge
Spring Cache @Cacheable (memoization)
Kafka Event-driven listeners
AI Agent Orchestration Agent dependency DAG

The key is a shift in mindset:

  • Don't specify order. Declare dependencies.
  • Caching isn't optimization. It's essential to structural programming.
  • Execution order isn't designed—it emerges.

Bottom-up is where we start.
Top-down is where we grow.


References

Top comments (0)