Diagrams from Java
Understanding the structure of a Java application becomes more challenging as the codebase grows. Generating diagrams from source code helps developers visualize important components, dependencies, and architectural relationships more clearly.
1. Introduction
Generating diagrams from Java code is a useful way to understand an application without manually examining every class. A diagram-generation process analyzes source code, extracts structural information, and converts it into a visual representation such as a UML class diagram or a higher-level architecture diagram. A typical flow is: Java Code -> Code Analysis -> Diagram Definition -> UML Diagram. Tools such as JavaParser can analyze Java source code, while PlantUML or Mermaid can describe and render the final diagram.
1.1 Why UML Diagrams?
UML diagrams provide a visual representation of classes and their relationships. In Java applications, they can show classes, interfaces, inheritance, dependencies, and associations without requiring developers to read the complete implementation. For example, if an OrderService uses an OrderRepository, the relationship can be represented as OrderService -> OrderRepository. UML diagrams are therefore useful for documentation, onboarding, code reviews, impact analysis, and understanding unfamiliar applications.
2. Generating UML Diagrams Using Static Code Analysis
Static code analysis examines source code without running the application. It can discover classes, interfaces, fields, methods, inheritance, implementations, and dependencies. This information provides the basic structure required to generate UML diagrams. JavaParser is one library that can be used for this purpose. It converts Java source code into an Abstract Syntax Tree (AST), allowing program elements to be inspected programmatically. The following simple application contains a DTO, repository, and service that will be used to demonstrate the process.
2.1 Creating the Order DTO
A Data Transfer Object (DTO) carries data between different parts of an application and generally contains little or no business logic. The following Order class stores an order ID.
public class Order {
private String id;
public Order(String id) {
this.id = id;
}
public String getId() {
return id;
}
}
The Order class contains an id field, a constructor to initialize it, and the getId() method to retrieve its value. These elements can later be represented as attributes and operations in a UML class diagram.
2.2 Defining the Order Repository
A repository defines how application data is accessed or stored and keeps data-access responsibilities separate from business logic. The following interface defines a simple contract for storing orders.
public interface OrderRepository {
void save(Order order);
}
The OrderRepository interface declares a save() method that accepts an Order object. This creates a relationship between the repository and the Order class that can be represented in UML.
2.3 Creating the Order Service
The service layer contains application business logic and coordinates other components. In this example, OrderService depends on the repository to store an order.
public class OrderService {
private final OrderRepository repository;
public OrderService(OrderRepository repository) {
this.repository = repository;
}
public void createOrder(Order order) {
repository.save(order);
}
}
The OrderService receives an OrderRepository through its constructor. Because the repository is stored as a field, static analysis can identify OrderRepository as a dependency of OrderService.
2.4 Analyzing Java Code With JavaParser
JavaParser examines Java code as structured program elements rather than plain text. Once a source file is parsed into an AST, the analyzer can search for classes, interfaces, fields, methods, and other declarations. The following DiagramGenerator demonstrates the basic idea by analyzing OrderService.java and printing its class name and field types.
import com.github.javaparser.StaticJavaParser;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import java.io.File;
public class DiagramGenerator {
public static void main(String[] args) throws Exception {
File file = new File("OrderService.java");
CompilationUnit code = StaticJavaParser.parse(file);
for (ClassOrInterfaceDeclaration type :
code.findAll(ClassOrInterfaceDeclaration.class)) {
System.out.println("Class: " + type.getNameAsString());
type.getFields().forEach(field ->
field.getVariables().forEach(variable ->
System.out.println(
"Dependency: " + variable.getTypeAsString()
)
)
);
}
}
}
The program first loads OrderService.java and parses it using StaticJavaParser.parse(). It finds class or interface declarations and then examines their fields. In this example, the field type reveals that OrderService depends on OrderRepository. This is intentionally a small analyzer. The same technique can be extended to scan all Java files in a project and inspect methods, interfaces, inheritance, annotations, and other relationships.
2.5 Running the Code and Generating UML Output
Running the analyzer against OrderService.java produces the following console output:
Class: OrderService Dependency: OrderRepository
This output confirms that the analyzer has identified OrderService and its dependency on OrderRepository. If the same analysis is applied to the other source files, the discovered information can be combined into a diagram definition such as the following PlantUML.
@startuml
class Order {
- id : String
+ getId() : String
}
interface OrderRepository {
+ save(order : Order) : void
}
class OrderService {
- repository : OrderRepository
+ createOrder(order : Order) : void
}
OrderService --> OrderRepository
OrderRepository --> Order
@enduml
PlantUML uses text-based syntax to describe diagram elements and relationships. When this definition is rendered, it produces a UML class diagram containing Order, OrderRepository, and OrderService. The diagram shows that OrderService depends on OrderRepository and that the repository works with the Order object.
3. Using Framework-Level Sources for Architecture Diagrams
Static analysis explains how Java classes are connected, but it does not always explain their architectural roles. Frameworks such as Spring Boot provide additional information through annotations and configuration. For example, Spring annotations can identify whether a component belongs to the web, service, or persistence layer. Consider the following simplified Spring Boot components:
// CONTROLLER
@RestController
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
}
// SERVICE
@Service
public class OrderService {
private final OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
}
// REPOSITORY
@Repository
public class OrderRepository {
}
The annotations @RestController, @Service, and @Repository provide architectural context. A framework-aware analyzer can inspect these annotations together with class dependencies to determine the role of each component.
3.1 Framework Analysis Output
After analyzing the annotations and dependencies, the extracted information could be represented in a simplified form such as:
Component: OrderController Type: REST Controller Depends On: OrderService Component: OrderService Type: Service Depends On: OrderRepository Component: OrderRepository Type: Repository
This analysis provides more context than class relationships alone. It identifies both how components are connected and the architectural layer to which each component belongs. The information can then be summarized as a higher-level architecture:
OrderController
|
↓
OrderService
|
↓
OrderRepository
|
↓
Database
Framework-level information can also come from application.properties, application.yml, JPA annotations such as @Entity, Spring bean configuration, pom.xml, and Gradle build files. Combining these sources with static analysis provides a more complete architectural view of the application.
4. Agent-in-the-Loop Generation
Static and framework analysis can produce a large amount of information in real-world applications. If every discovered class and relationship is included, the resulting diagram may become difficult to read. Agent-in-the-loop generation introduces an AI agent that helps organize and simplify the extracted information. The important distinction is that static analysis discovers the factual relationships, while the agent decides how those relationships should be presented. A typical flow is: Java Code -> Static Analysis -> Structured Information -> AI Agent -> Diagram. For example, analysis may discover the following relationships:
OrderController --> OrderService OrderService --> OrderRepository OrderRepository --> Order
For a detailed UML diagram, these class-level relationships are useful. For an architecture diagram, however, an AI agent can combine them with framework information and create a simpler representation:
REST API | ↓ Service Layer | ↓ Repository Layer | ↓ Database
4.1 Agent-Generated Diagram Definition
After deciding on the appropriate level of abstraction, the agent can generate PlantUML or Mermaid syntax for the final architecture diagram. For example, it could generate the following PlantUML definition:
@startuml component "REST API" as API component "Service Layer" as Service component "Repository Layer" as Repository database "Database" as DB API --> Service Service --> Repository Repository --> DB @enduml
When rendered, this definition produces a higher-level diagram showing the flow from the REST API through the service and repository layers to the database. Unlike the detailed UML diagram in Section 2, this diagram focuses on architectural layers rather than individual classes and methods. The agent should not replace static analysis as the source of structural facts. Instead, it uses the information already extracted from the code to select important components, group related elements, and generate a clearer diagram.
4.2 Human Review
The generated diagram can be reviewed by a developer before it becomes part of the project documentation. Human review helps identify incorrect assumptions, remove unnecessary details, and add architectural context that may not be directly visible in the source code. This creates a practical workflow in which static analysis provides reliable code information, the AI agent provides abstraction, and the developer validates the final result.
5. Conclusion
Generating diagrams from Java code provides a practical way to understand and document application structure. Static code analysis can identify classes, interfaces, fields, and dependencies, while framework-level analysis adds information about the architectural roles of controllers, services, repositories, entities, and other components. Tools such as JavaParser can extract information from Java source code, while PlantUML or Mermaid can convert that information into visual diagrams. For larger applications, an AI agent can help transform detailed analysis into simpler architecture views without replacing the underlying static analysis. The overall process can therefore move from detailed to higher-level information: Java Source Code -> Static Analysis -> Framework Context -> AI-Assisted Abstraction -> Diagram. This approach keeps generated diagrams closely connected to the source code while making them easier for developers to understand and maintain.

