description: A developer's case study of inheriting and refactoring a 40,000-line Java Swing file in a production hospital system — without rewriting it.
For six months, one application slowed down my work every single day.
Every morning I'd open the hospital management system, wait for it to load, and mutter something unkind under my breath. My colleagues did the same. We blamed the database. We blamed the old servers. We blamed Java Swing for being Java Swing.
Then one afternoon I opened the source file. And I understood.
The File That Wouldn't Open
The entry point of the application was a single Java class called MainForm.java.
It was over 40,000 lines long. The file was 2 MB of plain text. My IDE warned me it was too large to open comfortably, and suggested switching to read-only mode.
That was my first clue.
The system had been built over years by multiple developers, with no shared convention and no documentation. Nobody currently on the team had written the original code. Nobody wanted to touch it. It worked, most of the time, and that was enough.
Until you had to change something.
Layer One: The 900 Buttons
Once I could actually read the file, I started counting things. The first number that shocked me was the button count.
The application had a main menu with over 900 buttons — each one opening a different form, module, or dialog. That part made sense: it's a hospital system, there are a lot of features.
What didn't make sense was how they were declared:
documentCategoryButton = new BigButton();
documentCategoryButton.setIcon(new ImageIcon(getClass().getResource("/icons/document-open.png")));
documentCategoryButton.setText("Document Category");
documentCategoryButton.setIconTextGap(0);
documentCategoryButton.setName("documentCategoryButton");
documentCategoryButton.setPreferredSize(new Dimension(200, 90));
documentCategoryButton.addActionListener(this::onDocumentCategoryClicked);
documentTypeButton = new BigButton();
documentTypeButton.setIcon(new ImageIcon(getClass().getResource("/icons/document-type.png")));
documentTypeButton.setText("Document Type");
documentTypeButton.setIconTextGap(0);
documentTypeButton.setName("documentTypeButton");
documentTypeButton.setPreferredSize(new Dimension(200, 90));
documentTypeButton.addActionListener(this::onDocumentTypeClicked);
Every single one of the 900 buttons was declared like this. By hand. Seven lines per button. Over 6,000 lines just to create the buttons — before any of them actually did anything.
And every button had its own action handler, with a nearly identical body:
private void onDocumentCategoryClicked(ActionEvent evt) {
closeOpenDialogs();
this.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
DocumentCategoryForm form = new DocumentCategoryForm(this, false);
form.validateAccess();
form.clearFields();
form.setSize(mainPanel.getWidth(), mainPanel.getHeight());
form.setLocationRelativeTo(mainPanel);
form.setVisible(true);
homeDialog.dispose();
this.setCursor(Cursor.getDefaultCursor());
}
900 buttons. 900 declarations. 900 handlers. All doing the same thing with different class names.
Layer Two: The Triple Filter
The menu had three ways to filter buttons: empty search (show all), active search (filter by label), and category selection (show one category).
Three filter modes. I expected three short methods.
What I found was three methods, each roughly 5,000 lines long, with structurally identical bodies. Here's a slice of one:
private void filterBySearch() {
visibleMenuCount = 0;
if (access.canViewDocumentCollection()) {
if (documentCollectionButton.getText().toLowerCase().trim()
.contains(searchField.getText().toLowerCase().trim())) {
menuPanel.add(documentCollectionButton);
visibleMenuCount++;
}
}
if (access.canViewDocumentCategory()) {
if (documentCategoryButton.getText().toLowerCase().trim()
.contains(searchField.getText().toLowerCase().trim())) {
menuPanel.add(documentCategoryButton);
visibleMenuCount++;
}
}
// ... repeated for every button, three times over
}
Every button appeared in every filter method. If you wanted to change how a single permission worked, you had to find and edit it in three separate locations — across roughly 15,000 lines of nearly identical code.
This is the part where I stopped being angry and started being curious.
Layer Three: The Trap
The obvious move was to extract everything into a single data structure — an enum, a list, a registry — and let the UI iterate over it. I sketched that solution in my head and almost started typing.
Then I hit this handler:
private void onInsuranceClaimManualClicked(ActionEvent evt) {
if (access.getRoleCode().equals("SuperAdmin")) {
currentPage = "ManualClaimEntry";
closeOpenDialogs();
homeDialog.dispose();
idSearchDialog.setVisible(true);
} else {
String coderId = Database.queryScalar(
"SELECT coder_id FROM insurance_coder_registry WHERE user_id = ?",
access.getRoleCode()
);
if (!coderId.equals("")) {
externalClaimDialog.loadURL("http://" + Config.getWebHost() + "...");
externalClaimDialog.setVisible(true);
homeDialog.dispose();
} else {
JOptionPane.showMessageDialog(null, "Coder ID not found...");
}
}
}
Role branching. Database lookup. URL construction. A fallback dialog. This button was not the same as the other 899.
If I'd moved it into a generic pattern, I'd have silently broken behavior that someone depended on, somewhere in the hospital, probably at 2 AM on a bad night.
This is where most refactors go wrong. You find a pattern in the noise and force everything into it — including the three or four cases that were never part of the pattern to begin with.
What I Built
I ended up separating three concerns that had been living in one class.
1. Declarative menu definitions. Every entry — what it is, who can see it, what it opens — became a line in an enum:
ROOM_INFORMATION(
"Room Information",
"/icons/room.png",
RoomInformationForm.class,
access::canViewRoomInformation,
Category.A
),
2. Explicit special cases. Entries that needed custom logic took an explicit action parameter, so the weirdness stayed visible:
INSURANCE_CLAIM_MANUAL(
"Insurance Claim — Manual Entry",
"/icons/claim.png",
InsuranceClaimForm.class,
access::canProcessInsuranceClaimManual,
Category.F,
() -> openInsuranceClaimManual() // explicit, not hidden
),
3. A renderer that knows nothing. A new class read the enum and built buttons on demand. It had no idea what any of them did:
private JButton createMenuButton(MenuItem item) {
JButton button = new GradientButton(
item.label,
new ImageIcon(getClass().getResource(item.iconPath))
);
button.addActionListener(e -> executeMenuItem(item));
return button;
}
4. A single filter. Three 5,000-line methods became one stream:
public List<MenuItem> getFilteredItems(Category category, String searchText) {
return allItems.stream()
.filter(item -> item.permission.getAsBoolean())
.filter(item -> category == Category.ALL || item.category == category)
.filter(item -> searchText.isEmpty() ||
item.label.toLowerCase().contains(searchText.toLowerCase()))
.collect(Collectors.toList());
}
What Actually Changed
| Metric | Before | After |
|---|---|---|
| Button declarations | 900+ manual | 1 enum entry each |
| Access filter methods | 3 identical (~5,000 LOC each) | 1 stream (~10 LOC) |
| Adding a new menu item | 4 places to edit | 1 enum constant |
| Hidden logic | Scattered across 900 handlers | Explicit action parameter |
MainForm responsibility |
Everything | Navigation entry point only |
I didn't measure startup time before and after. That wasn't the goal. The goal was to make the codebase something a human could reason about — and that part worked.
The system stayed in production the whole time. No rewrites. No framework migration. Still Java 8, still Swing, still JDBC. This is a hospital system used daily by real staff. Keeping it running was non-negotiable.
The Thing Nobody Teaches You
Here's what I learned from staring at that file:
Legacy code isn't bad code. It's code written under constraints you don't know about, by people who aren't there to explain. The duplication exists because someone needed something to work on a deadline. The 40,000-line file exists because every shortcut made sense in isolation.
The hardest part of refactoring isn't writing the new version. It's knowing which parts of the old version you're not allowed to touch — because they encode behavior that real users rely on, even if no one remembers why.
The insurance claim handler with its weird role check? Somebody built that because a specific admin needed it to work that way. If I'd flattened it into a generic pattern, I would have broken a workflow that keeps a hospital running.
When the system is live and patients depend on it, "works correctly" outranks "looks clean."
Every time.
Full Case Study
Code, before/after snapshots, and the full refactor walkthrough are on GitHub:
→ github.com/Mihaaq-arch/legacy-java-refactor-case-study
Working With Me
If your team is dealing with a legacy Java codebase that nobody wants to touch — or a monolithic file that's slowing down every release — I take on freelance modernization work.
My focus is refactoring production systems without rewriting them: reading code you didn't write, identifying structural patterns in noise, and making the changes small enough to ship without breaking anything.
Reach out on LinkedIn or open an issue on the repo above.
Thanks for reading. If this resonated with legacy code you've dealt with, I'd love to hear about it in the comments.``
Top comments (0)