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);
}
}
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.
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');
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.
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);
});
}
}
The code structure itself expresses the 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));
});
}
}
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
But in reality, many tasks were parallelizable:
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();
}
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.
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
}
}
Here, listeners are leaf nodes of the tree. They activate when events (button clicks, message arrivals) occur.
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:
-
Sequential Phase - Writing procedural code with
forandif - Structured Phase - Spring DI, layered architecture, design patterns
- 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.
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
- Johnston, W. M., et al. (2004). Advances in Dataflow Programming Languages. ACM Computing Surveys.
- Elliott, C., & Hudak, P. (1997). Functional Reactive Animation. ICFP '97.
- Project Reactor Documentation
- Spring WebFlux Reference







Top comments (0)