JVM application
2026
My City
Montréal publishes its road work and street obstructions as open data. A resident who wants to know what is being dug up on their street, or a contractor looking for work to bid on, has no way to read any of it.
- Reads two City of Montréal CKAN datasets and joins each obstruction back to the road work that caused it.
- Residents file and track work requests. Contractors filter those by category, borough or start date, then bid.
- A clean clone builds and tests with one command. 35 tests run offline in under a second.
- Java 17
- Maven
- JUnit 4
- Gson
- REST / JSON
- Montréal open data
What splitting a 780-line class turned up
- Resident.java
- 780 → 106 lines
- Menu.java
- 579 → 97 lines
- Test suite
- 9 → 35 tests
- Duplication defects found
- 6
The screens, an HTTP client, JSON parsing and the Scanner reads came out. What is left is a person's seven fields and the two operations that belong to them.
Both sign-in screens, both registration screens, both post-sign-in menus and five input validators came out. It now picks between signing in and registering.
Across 6 files, from 3. The old suite needed a live third-party API to pass; mvn test now needs neither a network nor a terminal.
Each one surfaced while collapsing copies of the same code. Listed in the commit that removed them.
The build file was the real fix. We built this for a team of three and it only ever compiled inside the IDEs we wrote it in. JUnit and Hamcrest sat in the repo as jars under lib/. Gson came in through an IntelliJ library entry pointing into somebody's local Downloads folder. So a fresh clone got you a wall of errors. It builds with Maven now and the jar it produces runs.
Two classes held most of the application. Resident was 780 lines: seven fields describing a person, plus every screen a resident could reach, an HTTP client for the city's data portal, the JSON parsing, the console output and the keyboard reads. Menu was 579. I pulled them apart along what each piece was actually responsible for. A data package for the city API, a ui package for the screens, and models that are just models. I moved the code. I did not rewrite it.
Six bugs fell out of that, all of them from duplication. My favourite: the list of ten work categories existed in three copies that had drifted apart. Option 8 read "Travaux résidentiel" on the search screen and "Travaux résidentiels" on the submission screen. So a resident could file a residential request and then never find it again. One enum now, and a test that fails the moment two labels stop matching.
The rest were the same shape. Closing a request removed by email from a map keyed by object, so it removed nothing and the request stayed open forever. Every screen opened its own Scanner on System.in, and a Scanner reads ahead, so one screen could swallow a line the next one was waiting for. Date parsing ran on the default SMART resolver, which happily accepts 31/02/2024 and hands back 29 February. The sign-in loop was written for three attempts but redirected after two, so the third could never happen.
The old suite had nine tests and one of them called the live donnees.montreal.ca API, so the build failed on any machine without a route to it. Splitting the parsing from the fetching sorted that out. Field mapping is covered against fixtures and runs offline. The check that the city has not renamed a column lives in an integration test under failsafe, where it can still catch a real change without breaking an offline build.
