Skip to content
ExponaBLOG
ENGINEERING CRAFT · DAY 090 / 100

Using AI in the engineering workflow in Engineering Craft

How I use AI assistants without lowering quality.

Expona Admin7 min read

We're at Day 90 of my 100-day Engineering Craft series, and I want to address something that's become part of my daily workflow: using AI in the engineering workflow without sacrificing code quality or security. I've watched teams move from dismissing AI entirely to leaning on it so hard they lose ownership of their output—and I've found a middle ground that actually works.

The Real Promise of AI: Scaffolding, Not Thinking

When I say AI is great for scaffolding, I mean it excels at generating structure and boilerplate—the repetitive parts that consume time but don't require deep architectural decisions. I'm talking about database migrations, test file templates, CRUD endpoints, and Docker configurations. These are places where pattern recognition is genuinely useful.

Last month, I needed to set up a PostgreSQL schema with migrations for a new microservice. Instead of writing 200 lines of migration SQL from scratch, I described the entity relationships to Claude, got a solid starting point in 30 seconds, and spent my real energy on the edge cases and performance considerations. The AI got the basic structure right—foreign keys, indexes, column types—but I reviewed every line and added custom constraints specific to our business logic.

The same applies to test scaffolding. Unit test boilerplate is mechanical: setup fixtures, arrange state, act on the function, assert expectations. AI can generate this structure quickly:

typescript
describe('UserService.createUser', () => {
  let service: UserService;
  let repository: Repository<User>;
  let emailQueue: EmailQueue;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        UserService,
        { provide: UserRepository, useValue: { save: jest.fn() } },
        { provide: EmailQueue, useValue: { enqueue: jest.fn() } },
      ],
    }).compile();

    service = module.get<UserService>(UserService);
    repository = module.get(UserRepository);
    emailQueue = module.get(EmailQueue);
  });

  it('should create a user and send welcome email', async () => {
    const input = { email: 'test@example.com', name: 'Test User' };
    const expected = { id: '123', ...input };
    
    jest.spyOn(repository, 'save').mockResolvedValue(expected);

    const result = await service.createUser(input);

    expect(result).toEqual(expected);
    expect(emailQueue.enqueue).toHaveBeenCalledWith({
      to: input.email,
      template: 'welcome',
    });
  });
});

That took AI seconds. My job was to verify the mocks match our actual dependencies, add the specific test cases that matter for our domain, and catch the fact that we need to mock transaction behavior because our real service wraps creates in a database transaction.

First drafts matter too. When starting a new feature, I'll ask AI to outline the controller, service, and repository layers for a feature endpoint. It won't be perfect—it rarely handles our specific error codes, logging patterns, or database transaction logic—but it gives me a skeleton I can reason about and improve rather than starting from a blank screen.

Verify Everything—AI Isn't a Peer, It's a Tool

This is where most teams stumble. They treat AI-generated code like they'd treat code from another engineer: maybe skim it, merge it if it looks reasonable. That's wrong. AI-generated code needs the same rigor as any external dependency, because it often contains subtle bugs.

I've caught AI confidently suggesting SQL that would cause N+1 query problems, NestJS dependency injection setups that don't account for async initialization, and React components that unmount hooks at the wrong time. The AI doesn't know your system's constraints, your performance requirements, or your team's standards. It just knows patterns from training data.

Here's a concrete example from last month. I asked Claude to write a caching layer for a trading service:

typescript
async getMarketPrice(symbol: string): Promise<number> {
  const cached = await this.redis.get(`price:${symbol}`);
  if (cached) return parseFloat(cached);

  const price = await this.marketApi.fetch(symbol);
  await this.redis.set(`price:${symbol}`, price, 'EX', 300);
  return price;
}

Looks reasonable—cache check, fallback to API, store for 5 minutes. But I caught two problems:

  1. Cache stampede risk: If the cache expires during high load, multiple requests will simultaneously call the API. We needed a lock or a "stale-while-revalidate" pattern.
  2. No error handling: If the API call fails, we'd throw and lose the cache benefit. For pricing data, we should return stale data or a fallback during outages.

The corrected version uses a distributed lock and graceful degradation—logic that requires domain knowledge about trade execution, not just caching patterns. AI helped scaffold the function signature and the happy path. I added the production-ready thinking.

My rule: review AI code with the same skepticism I'd apply to a junior developer's first PR. Look for off-by-one errors, missing null checks, unhandled exceptions, performance implications, and security gaps. That's not pessimism—it's professionalism.

Keep Context Private—Secrets and Confidential Data Stay Local

This one's non-negotiable. I never paste production data, API keys, customer information, or proprietary business logic into AI assistants. Period.

Here's what I do: I anonymize or synthesize the problem. Instead of pasting real SQL showing our customer's pricing structure, I describe the schema in generic terms. Instead of asking "how do I fix this bug in our ride-hailing surge algorithm," I ask "how do I optimize a geospatial query with time-based weighting" and supply fake data.

Last quarter, I needed help optimizing a WebSocket reconnection strategy for a live trading feed. I couldn't share our actual protocol or error codes. So I wrote a generic example:

typescript
// Anonymized version I shared with AI:
// "I have a WebSocket that connects to a market feed.
// Connection failures are common. How would you implement
// exponential backoff with jitter, and how do you ensure
// old connection attempts don't interfere with new ones?"

// Then I got back a pattern, adapted it with our specific
// error handling and queue logic, and tested it in our environment.

This takes slightly longer than dumping your real code and asking for a fix, but it's worth it. Your architecture, your data contracts, your scaling patterns—those are competitive advantages or security boundaries. Keep them local. Use AI for the general engineering problem, not your specific implementation.

If your company has a policy about AI tool usage, follow it. Some organizations prohibit cloud-based AI for compliance reasons. If that's you, explore self-hosted options like Ollama with local models, or pay for tools with SOC 2 compliance and data retention policies you can audit.

Learn, Don't Outsource Thinking—Own Your Decisions

The most dangerous misuse of AI is abdicating technical decisions. "Should we use PostgreSQL or MongoDB?" isn't a question to ask an AI once and accept the answer. That's a decision that requires understanding your query patterns, consistency requirements, team skills, and operational overhead.

What AI is genuinely good for: exploring tradeoffs. I'll ask Claude to explain the consistency guarantees of event sourcing versus CQRS, or walk me through when you'd choose gRPC over REST. Then I read the response, think about it, research specific details that matter for my context, and decide. The AI has helped me learn faster, not replaced my judgment.

Similarly, when I ask AI to explain why a test is failing, I'm not looking for "here's the fix"—I'm looking for "here's why this might happen," and then I verify each hypothesis against my code. This is learning in real time. It's the difference between copying a fix and understanding a problem.

A practical discipline: I keep a log of AI suggestions I rejected and why. It keeps me honest about whether I'm using AI as a thinking partner or a shortcut. Looking back, most rejections fall into a few categories: missed edge cases, performance assumptions that don't fit our scale, or domain logic the AI couldn't know. That pattern tells me where to focus my AI usage—scaffolding and learning, not decision-making.

Common Mistakes

  • Committing untested AI code: Just because an LLM generated it doesn't mean it works. Run it locally, think through edge cases, test against your actual dependencies.
  • Treating AI responses as authoritative: AI is confident and fluent. That confidence is not correlation with correctness. Verify claims, especially about libraries, security, or performance.
  • Asking about your production architecture in chat: Even with "delete after 30 days" policies, assume anything you paste to a cloud AI is visible to the company and potentially logged. Don't share secrets.
  • Using AI to skip learning fundamentals: If you ask AI to generate a message queue consumer and don't understand how idempotency works, you've built a bug factory. AI scaffolds; you own understanding.
  • Not reviewing the reasoning: AI can explain *why* a solution works, but that explanation can be wrong. Verify it against your knowledge and documentation.

Key Takeaways

  • Use AI for scaffolding—boilerplate, tests, configuration, and first drafts save real hours without compromising quality.
  • Review AI code with professional skepticism; it's a tool, not a peer. Test it, reason about edge cases, and verify performance.
  • Never paste secrets, customer data, or proprietary logic into AI systems. Anonymize the problem, not the solution.
  • Use AI to learn faster—explanations, exploration, and examples—but own your architectural decisions and verify key claims yourself.
  • Track which AI suggestions you reject and why. That pattern reveals where AI genuinely helps and where you need to think for yourself.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles