Is your app growing, but problems are growing faster?

Cesar Mauri logo

Auditing an app doesn’t always mean spending months analyzing every line of code. Sometimes, what you need is an Express Audit: a clear, actionable, and prioritized diagnosis to know where to focus first.

This is the process I follow to identify what’s holding a project back:

1. Active Listening (The Context)

Everything starts with talking to the client. This is where the real pain points emerge:

  • “The app crashes and we don’t know why.”
  • “Every new feature takes twice as long as the last one.”
  • “Users leave negative reviews, but we don’t understand why.”
  • “Retention is low and we don’t know where they’re dropping off.”

2. Observability and Real Usage

Before diving into the code, I put on the user’s hat. I test critical flows (onboarding, checkout, etc.) and then analyze the data:

  • Logs: Is there noise or total silence?
  • Metrics: What are we measuring and what are we misinterpreting?
  • Errors: Identifying crashes and ANRs (for mobile apps).

3. Hybrid Analysis: AI + Human Judgment

I use AI agents for an initial sweep of security, maintainability, and performance. It’s an incredible tool for a quick, high-level overview.

But AI has its limits. That’s why I then perform a manual review looking for patterns:

  • Does the architecture support or block growth?
  • Are there unnecessary couplings that will create technical debt?
  • Are responsibilities being mixed between layers?

The Final Result

I don’t deliver a 100-page PDF that no one reads. Instead, I provide:

✅ A prioritized action list (urgent vs. important).

✅ A short video summarizing key findings in a personal way.

The goal isn’t absolute depth, but strategic breadth. To detect the main “fire” so the team can move forward with confidence.

🚀 Do you feel your app is stuck in a technical deadlock? Reach out and let’s talk about how I can help you unlock it.

What It Means to Be a Mobile Developer Who Also Thinks Like a CTO

Cesar Mauri logo

A 10-Minute Decision That Almost Cost a Project

Some time ago, a client asked me for help to unblock the development of a mobile application. The issue was data persistence: the app had to allow inspections to be collected in areas without network coverage, which meant the data had to be stored temporarily on the device. Initially, a relational database was chosen—a decision that seemed technically sound.

The problem: the data was structurally closer to documents than to rigid tables. This, combined with constant changes in requirements, turned the architecture into a nightmare. Every time a field was added or a form was modified, the team had to perform complex database migrations and manual transformations that consumed a lot of time and generated constant errors.

The solution I proposed: store the data in JSON files—more than sufficient to hold the information temporarily until it was time to transmit it.

Code Is a Liability, the Product Is the Asset

A mobile developer with a CTO mindset doesn’t just write code that works; they write code that builds a company. The difference lies in moving from “How do I implement this?” to “Why are we doing this, and at what cost?”

This is how a strategic profile thinks:

  • Code as debt: They understand that every line of code written is something that will need to be maintained and paid for in the future. Knowing when not to code is just as valuable as knowing how to do it.
  • “Boring” but solid technology: Even if they’re passionate about innovation, they prefer mature technologies. Why? Because it’s easier to find talent, they have fewer unexpected bugs, and they ensure the app will still be alive in two years.
  • An “App Store” mindset: Unlike the web, a mobile app isn’t updated instantly. A production bug can take days to fix due to Apple or Google review times and user update cycles. Thinking like a CTO means much stricter risk management before each release and valuing techniques (e.g., feature flags) that allow app behavior to change without publishing a new version.
  • Balancing speed and debt: They know that shortcuts can be taken in an MVP (Minimum Viable Product) to validate the market, but they can clearly identify when it’s time to “clean the house” so the app doesn’t collapse under its own weight months later.

Common Mistakes That Kill Startups and SMEs

  • Ignoring critical technical uncertainty: If your value depends on a technical innovation or demanding performance requirements, don’t build the entire app at once. Create a three-day proof of concept. If the technology doesn’t deliver, you won’t have lost three months.
  • Waiting too long to validate the business model: Especially critical if your app proposes a highly innovative feature that the market may never truly value.
  • Over-engineering: Software architectures and design patterns are powerful tools that should be used judiciously. Ignoring them is just as harmful as applying them blindly.

How to Spot (or Train) This Mindset

Developing this vision doesn’t happen overnight; it’s a shift in focus. If you want to know whether your team has this “chip,” or you want to help them develop it, look for these signals:

Traditional approach“Thinking like a CTO” approach
“We need to refactor this module.”“If we invest 2 days cleaning up this module, the next features will ship 30% faster.”
“Let’s use the latest library that came out yesterday.”“Does this library have a community? Will it be supported in 2 years? Is it secure?”
“I’ve finished the task according to the ticket.”“I’ve noticed this feature doesn’t help this month’s user retention goal—are we sure we want it this way?”

Fall in love with the problem, not the solution. If a user’s problem can be solved with a simple screen instead of a complex 3D animation—and that saves 40 hours of budget—the “Developer-CTO” will always choose the savings and reinvest them in what actually generates revenue.

Discovering Gemini CLI

Recently, Google launched Gemini CLI, an open-source AI agent for the command line.

In the announcement, they mention that the tool isn’t just for coding—it supports many other tasks as well. It also comes with very generous usage limits for the free version.

One feature that especially caught my attention is the ability to access web content and invoke Gemini CLI as part of a script.

In addition, Gemini CLI complements Google’s programming assistant, Gemini Code Assist, by providing an agent mode. Gemini Code Assist integrates into the IDE, which is definitely another reason to give this tool a try—I’ve added it to my to-test list.

It looks like Google is making a serious push to position itself as a major player in the AI-powered developer tools space.

Let’s Get Started!

I head over to the project’s repo: https://github.com/google-gemini/gemini-cli

First Impressions

The README highlights features like support for large codebases, task automation, the ability to analyze PDFs or images, and integration with tools via MCP for generating images or performing Google searches.

Installation

Gemini CLI is written in TypeScript and requires Node.js 18 or higher.

In my case, I install the tool on both of my usual work environments—Ubuntu Linux and macOS—where I already have Node.js 22.14.0 installed (with nve), using the following commands:

npm install -g @google/gemini-cli
gemini

There’s also an option to run Gemini CLI without installing it:

npx https://github.com/google-gemini/gemini-cli

It welcomes me with a screen where I can choose a theme. From the available options, I pick Dracula, one of the dark themes with more contrast than the default.

Next, it prompts me to authenticate. I choose Login with Google, confirm in the browser, return to the terminal—and hit my first issue:

Failed to login. Message: This account requires setting the 
GOOGLE_CLOUD_PROJECT env var. See https://goo.gle/gemini-cli-auth-docs#workspace-gca

Apparently, the issue is that the account I used is linked to Google Workspace. I switch to another account, and finally, the tool becomes usable.

Commands

Before diving into prompts, I type a forward slash / to display the list of commands:

These stand out to me:

  • /chat <list|save|resume> [tag] to manage conversations—similar to chat management in Gemini, I assume.
  • /compress compresses the context and replaces it with a summary—very interesting, curious to see how it performs with code.
  • /editor lets you choose the external editor—I’m sticking with Vim for now.
  • /mcp shows configured MCP servers—none yet.
  • /memory <show|refresh|add> to manage memory stored in files (e.g., GEMINI.md)
  • /tools shows what tools Gemini CLI has access to. Includes tools for filesystem access, downloading web content, running Google searches, and executing shell commands.
  • /help shows available commands and notes that you can refer to files using @ (e.g., @src/myFile.ts) or execute shell commands using ! (e.g., !git diff)

There’s no obvious way to change the model, which defaults to gemini-2.5-pro. So I check the gemini command options and—voilà—-m allows model selection. However, it’s unclear which models are supported. After a few Google searches, I find that you can pick a model if you have an API key from Google AI Studio, but you can also switch to the flash model with:

gemini -m gemini-2.5-flash

In any case, the usage limits for the pro version are generous (in theory) and more than enough for many use cases.

Testing Memory

Let’s try the memory. I tell it to remember my name:

/memory add My name is Cesar

This creates a .gemini/GEMINI.md file in my home directory, with a line containing just that info.

The surprise: when I ask for my name, it says it doesn’t know—even though it indicates it’s using the memory file:

> What is my name?
✦ I do not know your name. I am a large language model and do not have access to personal information about you.

Let’s try a fact:

/memory add Our sales in 2024 were 2.5 million
What were our sales in 2024?
I do not have access to your sales data for 2024

Still nothing. Strange.

> Dump the facts in your memory
✦ I cannot directly dump the facts in my memory. I can save information using the save_memory tool, but there is no tool available for me to retrieve or display what has been saved.

Something’s definitely not working.

I close and reopen the tool, and now it seems to be using memory:

> What is my name?
✦ Your name is Cesar.

But when adding new information, the same issue reappears. I thought it might be intentional, but the tool’s feedback says otherwise:

ℹ Memory refreshed successfully. Loaded 167 characters from 1 file(s).

Screenshot of the GEMINI CLI terminal saying that the memory has been refreshed but that cannot remmeber my name

Let’s check the docs. I type /docs and it opens the browser. Everything I find says /memory refresh should load data into context—but it’s clearly not working right.

I search the GitHub issues and find a vaguely related one, so I go ahead and open a new issue.

Hierarchical Memory

Project memory is organized using GEMINI.md files in plain Markdown.
There’s a global file in the user’s home directory, and project-specific files.

Within a project, there’s usually one in the root for general info, and others in subdirectories with more specific details.

When reasoning is required, the files are concatenated in order, from general to specific—so local definitions override global ones.

Example: the general convention is UpperCamelCase, but files in the assets folder use snake_case.


You define the general rule in the root GEMINI.md, and the exception in a GEMINI.md inside the assets folder.

Let’s Try a Real Project

Analyzing the Project

I run gemini inside a project’s source directory.

First, I ask:

What is this project?

It reads three files (NEWS, app/build.gradle.kts, and settings.gradle.kts) and correctly infers many details (omitted for confidentiality) and the tech used:

The application is built using modern Android technologies, including Kotlin, Jetpack Compose for the UI, Hilt for dependency injection, Room for the local database, and MapLibre for maps.

Now let’s ask it to generate the hierarchical memory files:

Perform an exhaustive analysis of the code base and create the hierarchical memory files to retain key details of the project. Generate GEMINI.md to be committed as part of the project.

It creates a root GEMINI.md file with a technical report including:

  • Project overview (purpose, main features by user role, core technologies)
  • Code structure (organization, architecture like MVVM and a Clean Architecture variant)
  • Build and test (build variants, test methodologies, key files)

It’s more of a report than I expected—I was hoping for something shorter and split across multiple files with more precise, context-usable info. Still, it’s worth testing.

Generating Code

Let’s test it with a basic task: writing unit tests for a mapper class:

Add tests for @app/src/main/java/org/gc/octopus/app/data/mappers/GearCompositeWithStateMapper.kt in @app/src/test/java/org/gc/octopus/app/data/mappers/GearCompositeWithStateMapperTest.kt

I kept the prompt as simple as possible to see if it would infer and follow the project’s style.

Using @ triggers a file search. It’s easier than typing full paths, though the results can be polluted with temp or generated files.

It works quickly, reads other test files, and the previewed code follows the existing style. I accept the changes, and it stops there.

I review the generated code—there’s a compile error. I ask:

The code does not compile. Fix it.

It asks permission to run gradlew, which I allow. It works for about 4 minutes, iterating until it finishes.

The result is solid and fits the project’s style well. I notice one test case is missing, so I ask:

Add a test to verify when elements is not empty

This time it hangs on Planning the implementation. After 5 minutes, I hit CTRL+C twice, which kills the session. Oops!

Skipping the details—but in a new session I finally get the test added.

Other Uses

Calling from a Script

You can run gemini from a script or pipe input to it. For example:

$ sudo dmesg | gemini -p "This is a dmesg output. Provide a hardware summary" -m gemini-2.5-flash

Result:

Here’s a summary of your hardware based on the provided dmesg output:

Motherboard:

  • Manufacturer: Gigabyte Technology Co., Ltd.
  • Model: B760M DS3H DDR4
  • BIOS Version: F19 (dated 09/27/2024)

Processor (CPU):

  • Model: 13th Gen Intel(R) Core(TM) i7-13700
  • Cores/Threads: 24
  • Clock Speed: 2.1 GHz (TSC detected at 2112.000 MHz)

Memory (RAM):

  • Total RAM: 32 GB

General Use

For general use, the Gemini web chat is more practical—auto chats, canvas, other options. Still, the CLI can be handy for quick queries—and it (supposedly) has more generous limits.

Google Searches

Let’s try:

Search online the weather forecast for the next week in Tarragona and provide a summary

Works fine:

GoogleSearch Searching the web for: “weather forecast next week Tarrago…
✦ Here is the weather forecast for Tarragona for the next week:

  • June 30: Sunny, 25–32°C
  • July 1: Mostly sunny, 25–35°C
  • July 2: Scattered clouds, 25–33°C
  • July 3: Sunny, 24–31°C
  • July 4: Clouds + chance of thunderstorms, 23–32°C
  • July 5: Light rain chance, 22–31°C
  • July 6: Possible light rain, 22–30°C
  • July 7: 40% chance of rain, 21–27°C

Accessing Website Content

Provide a summary of the contents of my webpage: https://cesarmauri.com/

Works without issues.

And then I hit the first usage limit:

ℹ ⚡ Slow response times detected. Automatically switching from gemini-2.5-pro to gemini-2.5-flash for faster responses for the remainder of this session.
⚡ To avoid this you can either upgrade to Standard tier. See:
https://goo.gle/set-up-gemini-code-assist
⚡ Or you can utilize a Gemini API Key. See:
https://goo.gle/gemini-cli-docs-auth#gemini-api-key

Ouch. I seriously doubt I hit the supposed 1000 daily request limit. Maybe those “generous” limits aren’t so generous after all? We’ll see.

Conclusions

This first hands-on with Gemini CLI shows that Google is serious about competing in the AI-assisted development tools space. The tool is clearly developer-friendly: command-line-based, open-source, and extensible via scripts—giving developers more visibility and control over how it behaves.

While not fundamentally different from tools like Cursor or GitHub Copilot, it brings some unique features: the CLI interface and the file-based hierarchical memory system using GEMINI.md files, which—if used well—could significantly enhance context customization.

It also supports web access, shell command execution, and external data/file processing—something not all assistants offer. However, it currently only supports Gemini models. While Gemini 2.5 Pro performs well for coding tasks, this limitation may weigh against it versus more flexible alternatives. Personally, I often prefer Claude Sonnet 4 for coding—it produces better results in my experience.

The tool is still rough around the edges: some features are buggy or underdocumented, and using it still takes a bit of patience and trial-and-error.

That said, the (supposedly) generous free-tier limits and its focus on practical dev tasks make it an interesting tool to incorporate into the workflow—at least as a complement.

Are you married with your framework?

In the vibrant universe of software development, some topics, like true classics, always ignite passionate debate. We’re talking about those eternal discussions that divide the community: tabs or spaces? vi or emacs? Similarly, frameworks hold a privileged spot on this list of “things that stir up passions.”

Just like other fundamental tools (programming languages or text editors), frameworks leave no one indifferent. And for good reason, because they undeniably have a beneficial side.

The Friendly Face of Frameworks

There’s no denying that frameworks are an unquestionable help. When used correctly, they are a powerful tool that allows us to elevate the quality of the final product. Why? Because they offer us the opportunity to reuse common logic that has already been tested and polished. This translates into fewer errors and more efficient development.

Additionally, frameworks are excellent for establishing a guide, a way of working that promotes a common language among all users. They create a true ecosystem, acting as magnets that attract other developments and contributions, thus enriching the whole.

When the Framework Becomes a Problem

But like in any good story, there’s a turning point. The problem arises when the framework becomes the absolute center of our development, conditioning and ultimately negatively impacting our project.

Let me illustrate with an example. The vast majority of modern web frameworks (I’m thinking of Spring Boot, Django, or Laravel, with which I’ve had the pleasure of working) come with an integrated ORM (Object-Relational Mapping).

Often, ORM entities are used directly as domain models. For relatively simple CRUD (Create, Read, Update, Delete) projects, this can be a perfectly valid solution, even the best option. No need to be dogmatic. However, when we delve into more complex business domains, this practice often becomes a real liability.

Let’s see why:

  1. Rigidity in Domain Modeling When ORM entities are used as domain models, especially in projects with complex business logic, design freedom is diminished. Instead of using Plain Old Java Objects (POJOs) or similar plain classes that offer total flexibility to model the domain as best suits business needs, the developer is limited by the ORM’s restrictions and expectations. This can lead to a suboptimal design that prioritizes framework convenience over the true nature of the business.
  2. Difficulty Adapting to Changes or Evolving If our business logic is tightly intertwined with specific framework functionalities (like an ORM), any attempt to switch to another technology or even to update the framework to a major version can become a real nightmare. Every modification in the framework could demand significant changes in our domain code, which increases maintenance time and cost.
  3. Increased Complexity for Unit Tests The coupling between our domain models and the ORM makes unit testing pure business logic difficult. Often, it’s necessary to mock or initialize parts of the ORM for tests to work, rather than being able to test our domain classes in isolation. This not only slows down the development cycle but can also compromise the reliability of the tests.
  4. Excessive Framework Dependency Making the framework the “center” of our development makes us overly dependent on its design decisions and features. If the framework stops receiving support, or if a key functionality is removed or drastically modified, our project can find itself in a very complicated situation. Lower coupling would allow for a much simpler framework change or migration if necessary.

In summary, the main disadvantage lies in the loss of flexibility and control over our own business code. We are forced to adapt to the framework’s way of working, rather than designing a solution that is truly efficient and adaptable to the specific needs of our domain. Remember, the framework is just a detail.