There is something wonderfully recursive about this article.
I am writing it to celebrate a major milestone in the development of EZ-Blog, the application I built to replace the long-dead CityDesk software that originally created this website. But this article is also intended to become a test article for the next stages of EZ-Blog itself.
In other words, I am using EZ-Blog to write about building EZ-Blog while using that same development effort to move toward being able to publish articles like this one from my phone.
Even more interesting, the application is being developed through what has evolved into a surprisingly effective symbiotic development process between a human software architect and multiple AI systems.
The result has convinced me that there is a much better model for AI-assisted software development than simply asking an AI to "write the code."
The Milestone: From Monolith to Modular Architecture
EZ-Blog began as a relatively simple JavaFX desktop application.
Its purpose was straightforward: replace CityDesk, allow me to write articles using a WYSIWYG editor, generate my existing static website, and publish it.
That worked.
But my next goal created a much larger architectural problem:
I want to be able to add, preview, and publish an article from my iphone.
The easiest solution would have been to bolt a web server onto the existing application.
That would also have been the wrong solution.
Instead, we decided to first restructure the application into independently reusable components while preserving the existing desktop application.
After a lengthy series of small, verified refactorings, Phase 1 is now complete.
The original single-module application has become four Maven modules:
+----------------+
| ezblog-core |
|----------------|
| Domain model |
| Shared values |
| Slug policy |
+-------+--------+
^
+------------+------------+
| |
+---------+----------+ +---------+----------+
| ezblog-persistence | | ezblog-publisher |
|--------------------| |--------------------|
| Repository APIs | | Templates |
| YAML persistence | | Rendering |
| Content loading | | Site generation |
| Serialization | | Change detection |
+---------+----------+ +---------+----------+
^ ^
| |
+------------+------------+
|
+-------+--------+
| ezblog-desktop |
|----------------|
| JavaFX UI |
| Configuration |
| Composition |
| SFTP deploy |
+----------------+
The dependency direction is deliberate.
ezblog-core is framework-independent.
ezblog-persistence owns persistence contracts and the current YAML implementation.
ezblog-publisher owns deterministic rendering, templates, static-site generation, regeneration, and publishing policy.
ezblog-desktop owns the JavaFX user interface and assembles those capabilities into the existing desktop application.
And, critically, the shared publisher and persistence modules do not depend on the desktop application — or on each other.
That is what makes the next stages possible.
Eventually a Spring Boot server can use those same shared components:
Desktop Mobile Browser
| |
v v
+---------------+ +---------------+
| JavaFX Client | | Web Admin UI |
+-------+-------+ +-------+-------+
| |
| +------v------+
| | EZ-Blog |
| | Server |
| +------+------+
| |
+----------------+-----------------+
|
+------------+------------+
| |
v v
Persistence Publisher
\ /
\ /
+-------- Core -------+
The public website itself will remain static.
That means the future server is not replacing the public website with a dynamic application. It will instead act as a private content-management and publishing system that generates the same fast, simple static website.
How Do We Know the Refactoring Worked?
This is where the development methodology becomes important.
A refactoring is not successful because the code looks cleaner.
It is successful because we can produce evidence that the architecture changed while the required behavior did not.
At the completion of the modularization milestone:
-
280 automated tests pass with zero failures, errors, or skips.
-
SpotBugs reports zero findings or errors across the production modules.
-
JavaFX dependencies are confined to the desktop module.
-
SnakeYAML dependencies are confined to the persistence module.
-
JSch/SFTP dependencies are absent from core, persistence, and publisher.
-
Desktop runtime and packaged-application verification passes.
-
Manual JavaFX verification passes.
-
Live guarded SFTP integration testing verifies server identity, byte integrity, rollback, isolation, and cleanup.
Most importantly, the desktop application still works.
That last requirement governed the entire modularization effort.
We did not rewrite the application.
We progressively changed its internal architecture while continuously proving that the existing system remained operational.
AI-Assisted Development Is Not "Let the AI Write the Program"
One of the more interesting results of this project has actually been the development process itself.
I have been experimenting extensively with both ChatGPT and Codex, but I deliberately do not treat either AI as an autonomous software engineer whose output should simply be accepted.
Instead, three participants have distinct roles:
HUMAN ARCHITECT
goals / judgment / approval
/ \
v v
ChatGPT <----> Codex
analysis implementation
architecture repository work
review tests
\ /
\ /
v v
EVIDENCE
code / tests / build
I call the interaction between the two AI systems the ping-pong effect.
The process frequently looks something like this:
Human → ChatGPT
I discuss the architectural objective, constraints, alternatives, and desired next step with ChatGPT.
ChatGPT → Human
ChatGPT analyzes the architecture and helps formulate a tightly bounded implementation task.
Human → Codex
I give that task to Codex, which has direct repository context and performs the implementation.
Codex → Human
Codex reports exactly what it changed, the tests it ran, coverage results, and any architectural issues it discovered.
Human → ChatGPT
I bring the result back to ChatGPT for an independent architectural and engineering review.
ChatGPT → Human → Codex
Potential problems or improvements are turned into specific questions or corrective tasks for Codex.
Then the cycle repeats.
Define
|
v
+---------+ Implement +---------+
| ChatGPT | --------------------> | Codex |
+----+----+ +----+----+
^ |
| Review results |
+---------------------------------+
| |
+------------ HUMAN --------------+
judgment and decisions
This creates something extremely valuable:
Neither AI is allowed to be the final judge of its own work.
The Human Is Not Removed from the Loop
This is the part of AI-assisted development that I believe is frequently misunderstood.
The human role becomes more architectural, not irrelevant.
I decide:
-
what problem we are solving;
-
what constitutes an acceptable architecture;
-
which tradeoffs are acceptable;
-
when scope should change;
-
whether an AI recommendation makes sense;
-
whether evidence is sufficient;
-
and when we are ready to proceed.
There have been numerous cases where the AI recommended something and I challenged it.
There have also been cases where I proposed something and the AI convinced me there was a better approach.
That is exactly what I want.
The objective is not for the AI to obey me.
The objective is for the combined process to produce a better engineering result.
AI Output Is a Proposal — Evidence Is Proof
During this project we eventually formalized the lessons we were learning into a development standard.
The most important rule may be the simplest:
AI output is a proposal, not proof.
If Codex says the implementation works, that is not proof.
If ChatGPT says the architecture is correct, that is not proof.
If I think the design is correct, that is not proof either.
The evidence comes from the repository:
source code, tests, coverage, generated artifacts, dependency analysis, runtime behavior, official documentation, and reproducible commands.
That distinction dramatically changes how AI should be used for serious software engineering.
Best Practices We Developed
Several practices have emerged from the project that I believe apply broadly to AI-assisted software development.
1. Make Small, Reversible Changes
Do not tell an AI:
"Modularize this application."
That task is far too large.
Instead:
"Extract this specific capability behind this boundary, preserve these behaviors, and verify these tests."
Small changes make failures understandable and rollback practical.
2. Establish the Baseline First
Before changing anything, understand exactly what currently works.
We established a Git baseline, verified the desktop application, captured tests, documented the architecture, and preserved representative generated output.
You cannot reliably prove that behavior was preserved if you never established what the original behavior was.
3. Separate Structural Refactoring from Functional Change
During modularization we deliberately avoided adding SQLite, Spring Boot, mobile publishing, or other new features.
The objective was structural:
change the architecture without changing the behavior.
This dramatically reduces the number of variables involved when something breaks.
4. Characterize Before Moving
Legacy code is often poorly isolated.
Before moving risky code, create or strengthen tests that characterize its existing behavior.
Then move it.
Then run those tests again.
5. Move Before Redesigning
Moving and redesigning code simultaneously makes failures difficult to diagnose.
A safer sequence is:
Understand
↓
Test
↓
Move
↓
Verify
↓
Improve
This proved especially useful while extracting publishing and persistence behavior from the original desktop application.
6. Test Architectural Boundaries, Not Just Behavior
A normal unit test can prove that a method returns the right result.
It cannot necessarily prove that your architecture is actually modular.
We therefore also verify dependency boundaries.
For example:
JavaFX -> desktop only
SnakeYAML -> persistence only
JSch/SFTP -> desktop deployment only
Those boundaries are checked through source inspection, Maven dependencies, runtime classpaths, packaging tests, and other automated checks.
7. Use Coverage as a Diagnostic, Not a Score
High coverage is useful.
Blindly chasing 100 percent coverage is not.
Every uncovered branch should instead provoke a question:
Why isn't this being tested?
Sometimes the answer reveals missing behavior.
Sometimes the path is structurally unreachable.
Sometimes external integration requires a different type of test.
The important part is understanding the gap rather than gaming the percentage.
8. Independently Review AI-Generated Changes
This is where the ping-pong process becomes especially powerful.
The AI that implemented a change should not simply announce that its own work is correct.
Give the change to another reviewer.
Ask specifically about:
-
correctness;
-
missing tests;
-
architecture;
-
security;
-
operational risks;
-
documentation;
-
and hidden coupling.
Then resolve disagreements with evidence.
9. Stop When Verification Fails
This sounds obvious, but AI makes it remarkably easy to barrel forward.
Don't.
If a checkpoint fails:
STOP
↓
Diagnose
↓
Correct
↓
Repeat Verification
↓
Continue
Never stack three more changes on top of a failure you do not understand.
10. Keep Documentation Synchronized with Reality
Architecture documents that describe last week's architecture are worse than useless.
After significant changes we update the architecture, design, implementation plan, testing guidance, inventory, and implementation log.
Documentation is therefore part of the implementation rather than an archaeological record created months later.
11. Keep Humans at Material Decision Boundaries
Routine reversible work should proceed efficiently.
But destructive actions, security changes, external publication, architectural changes, scope expansion, deployments, and other material decisions should return to the human.
This creates an important balance:
AI autonomy for execution; human authority for consequential decisions.
The Symbiotic Effect
What surprised me most is that this process is not merely faster programming.
Each participant compensates for weaknesses in the others.
Codex is excellent at operating directly against the repository, inspecting call graphs, modifying code, running tests, and reporting concrete results.
ChatGPT is useful as an architectural partner with a broader conversational context — challenging assumptions, examining alternatives, reviewing Codex's conclusions, and helping determine what should happen next.
And the human supplies something neither AI possesses: ownership of intent and engineering judgment.
That creates a feedback system:
Human intent
↓
Architectural reasoning
↓
AI implementation
↓
Automated evidence
↓
Independent AI review
↓
Human judgment
↓
Refined intent
↺
The output of one stage becomes the input to another.
That is the ping-pong effect.
Instead of relying on a single AI conversation to produce the correct answer, we deliberately create opportunities for disagreement, correction, verification, and refinement.
The Irony of Going Slower to Go Faster
At first, this process can feel painfully slow. But remember the old saying, "Slow is Smooth and Smooth is FAST!"
Move one capability.
Run focused tests.
Inspect coverage.
Run regression tests.
Review dependencies.
Run the complete build.
Review the diff.
Cross-review the change.
Update documentation.
Commit.
Then move to the next capability.
But something interesting happens after enough iterations.
The architecture becomes cleaner.
The tests become stronger.
The boundaries become explicit.
The AI receives better context.
And each subsequent change becomes easier and safer.
We have also learned when not to proceed one microscopic command at a time. Routine, reversible verification can be grouped together once the implementation step has been approved. The fine-grained checkpoints are retained for architectural, destructive, external, or otherwise consequential decisions.
So the process itself has evolved.
We are not merely using AI to improve the software.
We are using the experience of developing the software to improve the way we use AI.
What's Next?
The modularization milestone was not the ultimate goal.
It was the foundation.
The next phase introduces SQLite persistence behind the repository abstractions we have now created.
After that comes the Spring Boot administrative server.
And eventually:
iPhone / Browser
|
v
EZ-Blog Server
|
+------+------+
| |
v v
SQLite Publisher
|
v
Static Website
At that point I should be able to take out my phone, write an article, preview it, and publish it to this website.
And the old JavaFX desktop application will still exist.
Both interfaces will ultimately operate over the same architectural foundation.
A Milestone Worth Celebrating
Software modularization is not glamorous.
There is no exciting new user interface to show for this phase. In fact, one of the primary success criteria was that the application should look and behave exactly as it did before we started.
But internally, it is now a fundamentally different system.
We have transformed a desktop application into a reusable architectural foundation while preserving its existing behavior.
And along the way we developed something else that may prove even more interesting: a disciplined model for human-AI collaborative software engineering.
Not "vibe coding."
Not blindly accepting generated code.
Not pretending the AI is infallible.
And not pretending the human is either.
Instead:
Human intent. AI reasoning. AI implementation. Independent review. Automated verification. Human judgment. Repeat.
That is the experiment.
And so far, it is working.
My Corner of the Web