At some point while building Security Lens, the main feature started working.
I could upload a file.
OCR extracted text.
The application detected potentially sensitive information.
AI could analyze additional privacy risks.
And the result came back through the UI.
For a moment, it felt like the MVP was basically done.
But I had already learned something from deployment.
A feature working is not the same thing as a product being ready to leave my machine.
So before calling the project finished, I started checking a different set of things.
Not just:
Does the code work?
But:
Can someone else actually use this without knowing anything about my development environment?
That changed what “done” meant.

1. First, it had to survive outside localhost
Before thinking about performance or product quality, I had to make sure the application could actually run somewhere other than my computer.
That meant dealing with things like:
- Port configuration
- Environment variables
- Docker
- Linux differences
- Build configuration
- Runtime logs
I ran into more problems here than I expected.
The application worked locally, but deployment exposed assumptions that had been hidden by my development environment.
I wrote about those problems separately because they became a topic of their own.
→ Spring Boot Works Locally but Fails After Deployment — What I Checked
Once the application could actually run outside my machine, the next question became more interesting.
Was the product itself ready?
2. Individual endpoints working wasn't enough
During development, it was easy to check things one piece at a time.
Does the upload endpoint respond?
Does OCR return text?
Does the PII detector find a phone number?
Does the AI request return a response?
Those checks were useful, but they didn't represent how someone would actually use Security Lens.
The real flow was closer to this:
Upload
↓
OCR
↓
PII Detection
↓
EXIF / Metadata Analysis
↓
AI Risk Analysis
↓
Validation
↓
Result
So I started testing the application as a complete flow instead of treating each component as an isolated success.
A working endpoint could still be part of a broken product.
The whole path had to work.
That sounds obvious now.
It didn't feel quite as obvious while I was building each part separately.
3. Then I wanted to know what would break first
Once the main flow was working, I also wanted to see how the application behaved when several requests arrived.
I used k6 to put some load on it.
I wasn't trying to prove that Security Lens could handle production-scale traffic.
That would have been a much bigger claim than the test could support.
I wanted something simpler:
What breaks first?
That question was useful.
OCR, file handling, and AI-related processing aren't free.
They consume memory and CPU, and multiple requests can make that much more noticeable.
During testing, I ran into resource pressure and an out-of-memory problem.
That was actually more useful than getting a clean graph and moving on.
It showed me that the current architecture had a limit.
The load test didn't give the MVP a “production ready” badge.
It showed me where the MVP stopped being comfortable.
4. I added concurrency control instead of pretending the limit didn't exist
One thing I looked at was the number of expensive operations that could run at the same time.
For that, I used a Java Semaphore.
Conceptually, the idea was simple.
Request
↓
Acquire permit
↓
Run expensive operation
↓
Release permit
Instead of letting every request immediately start the same memory-heavy work, I could restrict how many operations entered that section concurrently.
That doesn't magically create more memory.
It just gives the application a way to say:
Only this many expensive jobs should run at the same time.
For an MVP, that was useful.
There is an important limitation, though.
The semaphore controls concurrency inside one application instance.
If the application later runs across multiple instances, each JVM has its own semaphore.
So this is not a distributed lock or a global concurrency limit for the entire service.
I didn't need to solve every scaling problem at this stage.
But I did want to understand what problem my solution was actually solving.
5. AI created a different kind of boundary
Security Lens also forced me to think about where AI belonged in the pipeline.
It would have been easy to let the AI handle more.
OCR extracts something.
Send everything to the model.
Ask it to clean the text, interpret it, find personal information, fix mistakes, and generate the result.
That sounds convenient.
It also creates a problem.
If the model silently “fixes” the evidence, how do I know whether the result came from the uploaded file or from the model?
That matters much more in a privacy-related tool.
So the flow I ended up using looked closer to this:
OCR
↓
Deterministic Rules
↓
AI Analysis
↓
Validation
↓
Result
Some things should remain deterministic.
Phone number patterns.
Email patterns.
Known metadata.
The text that OCR actually returned.
AI can help interpret risk and add context.
But it shouldn't be allowed to rewrite the evidence and then pretend that rewritten version came from the original file.
That distinction became much clearer while building the actual product than it ever had while reading about AI architecture.
6. Error cases mattered more once the happy path worked
The happy path is satisfying.
Upload a valid file.
Everything works.
Result appears.
Done.
Except users don't only send the file I imagined while developing the feature.
So I also had to think about cases like:
- Unsupported files
- Empty or unreadable OCR results
- Missing metadata
- AI failures
- Unexpected input
- Large files
- External service failures
I didn't solve every possible edge case.
That wasn't the goal of the MVP.
But I wanted failure to be something I had at least considered, instead of something I discovered only after another person tried the service.
That was another shift in how I looked at “finished.”
7. I started separating “known limitation” from “unknown problem”
This became one of the more useful distinctions for me.
An MVP can have limitations.
Security Lens definitely does.
What bothered me more was not knowing what those limitations were.
There is a difference between:
This part currently has a concurrency limit.
and:
I have no idea what happens when multiple users do this.
There is also a difference between:
AI analysis can fail, and this is how the application handles it.
and:
I haven't checked what happens if the model doesn't respond.
I didn't need the MVP to be perfect.
I wanted to be able to explain its boundaries.
Once I could do that, the remaining problems felt much more manageable.
8. My definition of “done” changed
At the beginning, my checklist was mostly feature-oriented.
Upload works
OCR works
PII detection works
AI analysis works
UI works
By the end, it looked more like this:
Can it build cleanly?
Can it run outside my machine?
Does the complete user flow work?
What happens when something fails?
What happens under multiple requests?
Where are the resource limits?
Which parts are deterministic?
Which parts depend on AI?
Can I explain the known limitations?
That's a very different checklist.
And I think that's where the project changed for me.
I wasn't only checking whether I had implemented the features anymore.
I was checking whether I understood the thing I had built.
What I would check before shipping another Spring Boot MVP
If I build another small Spring Boot product, these are the questions I'll probably ask before calling it shipped:
- Does it build from a clean environment?
- Can it run without my local machine?
- Does the complete user flow work?
- Are environment-specific values separated from the source code?
- Are secrets kept out of the repository?
- What happens when an external dependency fails?
- What happens when several expensive requests arrive?
- Do I know the current memory and concurrency limits?
- Which results come from deterministic logic?
- Which results depend on AI?
- Are AI outputs validated before becoming product results?
- Can I clearly explain the limitations that still remain?
I also turned the broader release checks I collected during this process into a reusable checklist.
→ Ship Your Spring Boot MVP — Free Release Checklist on GitHub
And if you want to see the project behind these decisions:
→ Security Lens — Spring Boot Privacy Risk Analyzer Case Study
Shipping didn't mean everything was solved
I used to think an MVP was done when the important features worked.
Now I think that's only one part of it.
Security Lens still has things I want to improve.
There are better ways to handle concurrency.
There is more testing I could add.
The AI boundary can become stricter.
The privacy analysis itself can become more sophisticated.
But I no longer think “done” means there are no more problems.
For this MVP, it meant something closer to:
I know what works, I know what I tested, and I can explain what is still limited.
That was enough to ship it.
And for the first time, that felt very different from simply getting the code to run.
'Development Notes' 카테고리의 다른 글
| Spring Boot Works Locally but Fails After Deployment — Docker, Railway, and WSL Checks (0) | 2026.09.20 |
|---|