MoreRSS

site iconThe Practical DeveloperModify

A constructive and inclusive social network for software developers.
Please copy the RSS to your reader, or quickly subscribe to:

Inoreader Feedly Follow Feedbin Local Reader

Rss preview of Blog of The Practical Developer

Power Interview: All‑in‑One Interview Assistant—Transcription, AI Replies, Code Help, Face Swap

2026-02-13 13:13:22

📧 Email | 🌐 Website | 💬 Telegram | 💭 Discord

Interviews are high-stakes moments: you’re expected to think clearly, communicate confidently, and solve problems in real time—often while juggling nerves, time pressure, and technical challenges. Power Interview is built for that reality.

Power Interview is your personal AI-powered interview coach that supports you during live conversations with real-time transcription, context-aware reply suggestions, technical code assistance, and optional real-time face swap—all designed with one priority: your privacy stays with you.

Start free with 30 credits and pay with coins only (no credit card, PayPal, or bank account required).

Windows only for now (MacOS & Linux coming soon).

Why Candidates Choose Power Interview

1) Real-Time Support That Feels Like a Coach in Your Corner

Power Interview doesn’t just “practice” with you—it helps you in the moment by providing:

  • Live transcription with dual-channel speaker detection
  • Full conversation history so the AI remembers what was already said
  • Smart reply suggestions that help you answer clearly, professionally, and confidently

Whether you’re navigating behavioral questions (“Tell me about a time…”) or clarifying complex technical tradeoffs, Power Interview helps you stay structured and articulate.

2) Context-Aware Suggestions Built Around You

Generic interview advice is rarely useful. Power Interview’s AI reply suggestions can be grounded in:

  • Your CV / resume
  • The job description
  • Your role context and interview profile
  • The entire conversation history

The result: suggestions that match your experience, align with the role, and stay consistent with what you’ve already communicated—so you sound prepared, not scripted.

3) Code Test Assistance for Technical Interviews

When the interview turns into a live coding challenge, Power Interview can help you move faster and think clearer with:

  • Screenshot-based code problem analysis
  • LLM-powered solution guidance
  • Syntax-highlighted code suggestions

It’s built to support real technical interview workflows—not just practice exercises.

4) Advanced Stealth Mode (Designed for Real Interview Conditions)

Power Interview includes a stealth mode that focuses on discretion and control:

  • Hotkeys for instant access
  • Opacity controls and smart positioning
  • A window designed to be not capturable and invisible during full screen share

This is especially useful for maintaining a clean, professional screen-share experience.

5) Export Transcripts + AI Insights to Improve Faster

After the interview, Power Interview helps you turn every session into measurable growth:

  • Export transcripts to DOCX
  • Get AI-powered summaries and insights
  • Identify communication patterns (too brief, too long, unclear structure, missed opportunities)

Instead of guessing what went wrong (or right), you’ll know exactly what to improve next time.

6) Real-Time Face Swap (For Legal Use Only)

Power Interview offers real-time face swap through OBS integration—built for simple setup:

  • Install OBS Studio and VB-Cable
  • No complicated configuration required

This feature is powerful and must be used responsibly. Power Interview clearly states: For legal use only—do not impersonate others or misrepresent your identity.

Privacy First Means: Your Data Stays With You

Many tools store your interview data in the cloud. Power Interview takes a different approach:

  • Secure local storage
  • No data mining
  • No selling user data
  • Full control over your information

If privacy matters to you—especially when your resume, job targets, and interview conversations are involved—Power Interview is built with that priority from day one.

Simple, Transparent Pricing (Pay With Credits)

Power Interview uses a straightforward credit system:

  • 10 credits per minute while the assistant is running
  • Credits stop when you click Stop Assistant
  • New users get 30 free credits

Plans include:

  • Starter: $9 / 600 credits (~60 minutes)
  • Pro: $70 / 6,000 credits (~600 minutes)
  • Enterprise: $500 / 60,000 credits (~6,000 minutes)

And importantly: coins only—no credit card, PayPal, or bank required.

Get Started in Minutes

  1. Download and run the Power Interview desktop app
  2. Create an account (new users get 30 free credits)
  3. Add your resume/CV + job description to your interview profile
  4. Select your audio/video devices
  5. Click Start Assistant (credits begin only while running)
  6. Join your interview and get real-time support
  7. Stop the assistant when done and export your transcript + insights

Power Interview: Perform Better, Learn Faster, Stay Private

If you want a tool that helps you communicate more clearly, handle technical rounds, stay calm under pressure, and review your performance—without handing your personal data to third parties—Power Interview is built for you.

Download Power Interview and start with 30 free credits today.

📧 Email | 🌐 Website | 💬 Telegram | 💭 Discord

Building a Real-Time Air Quality Dashboard Using APIs

2026-02-13 13:12:42

Air pollution isn’t just an environmental issue anymore—it directly impacts our health, productivity, and everyday decisions. As developers, we have the ability to turn raw environmental data into something people can actually understand and use. That’s exactly what a real-time air quality dashboard does.

By integrating public air quality APIs, you can build a live dashboard that displays AQI levels, pollutant data, and location-based insights in a simple, visual way.

**
 **

A real-time air quality dashboard helps:

Make invisible pollution data visible

Track air quality changes across cities and regions

Support health-focused and smart city applications

Showcase real-world API and data visualization skills

It’s also an excellent project for developers looking to build something meaningful while strengthening their frontend and API integration experience.

How the Dashboard Works

Fetch Live Data
Pull real-time data from air quality APIs like OpenAQ or AirVisual.

Process & Interpret
Convert raw sensor values into AQI categories and health indicators.

Visualize Clearly
Use charts, maps, and color-coded indicators to show pollution levels at a glance.

Make It Interactive
Add city search, auto-refresh, and alert thresholds for a better user experience.

Tech Stack Ideas

Frontend: React, Vue, or Vanilla JavaScript

Backend (optional): Node.js or Python

Visualization: Chart.js, D3.js, Recharts

Maps: Mapbox or Google Maps

APIs: OpenAQ, IQAir, EPA datasets

Final Thoughts

This isn’t just another dashboard project. It’s a practical way to combine APIs, data visualization, and environmental impact into a single application. Projects like this show how developers can use technology to solve real-world problems.

Exploring Uttarakhand: Spiritual Sanctuaries and the Symphony of Nature

2026-02-13 13:10:54

India's Himalayan heartland, Uttarakhand, throbs with a rhythmic fusion of timeless wisdom, ethereal beauty, and limitless vitality. It is the epitome of the home of the gods, where snow-capped mountains cradle lush valleys and holy waterways flow with heavenly intent. It was created as a state in 2000 from Uttar Pradesh. Uttarakhand is a haven for the tired, beckoning exploration of its natural wonders, historical tales, and daring boundaries, from the peaceful murmurs of abandoned villages to the thunder of wild rivers. Its landscapes, which are influenced by spiritual legend and geological magnificence, offer a setting for both excitement and reflection. We highlight Uttarakhand's top attractions in this in-depth analysis, exploring their distinctive qualities, conservation tales, and must-see travel items. An amazing journey is guaranteed by Uttarakhand's harmonic symphony of environment and spirituality, regardless of your travel style—contemplative or adventurous.

Idyllic Hill Stations: Serene Escapes Amidst Peaks

The hill stations of Uttarakhand are peaceful havens where the ground whispers stories of past times and the air hums with bird music.

Chaukori: The Sunrise Village

At 2,010 meters, Chaukori is known as the "Sunrise Village" because of its unhindered views of the Himalayas, which include Trishul and Nanda Devi. Originally built as a British getaway, it has colonial homes and a boating lake called Chaukori. Treks to the Gwaldam Glacier give alpine thrills, while the Patal Bhuvaneshwar Cave nearby offers spelunking activities. Sustainable living is emphasized by the homestays and organic tea plants in the village. Chaukori is best visited in the fall for its golden foliage and is a great place for leisurely walks and photography.

Munsiyari: The Gateway to Panchachuli

At 2,200 meters, Munsiyari is a starting point for hikes to the summits and Panchachuli Glacier. It features the old Munsiyari Temple, the Birthi Falls, and the Khaliya Top viewpoint, all surrounded by birch woodlands. The town's festivals and monasteries are clear examples of its Indo-Tibetan culture. Adventurers love paragliding in the summer and skiing in the winter. Accessible through Pithoragarh, Munsiyari's secluded beauty makes it ideal for off-the-beaten-path excursions. Read more

Breaking the Edge Barrier: Why NetApp ONTAP is the Missing Piece of the AWS Local Zone Puzzle

2026-02-13 13:08:43

As organizations push the boundaries of real-time applications, AWS Local Zones have emerged as the premier solution for bringing compute and storage closer to the end-user. By placing infrastructure in metropolitan centers, AWS allows developers to achieve sub-10ms latency for workloads that simply cannot tolerate the round-trip time to a distant regional data center.

However, there is a "Local Zone Paradox": the closer you get to the user, the fewer AWS services are typically available. While you get the speed of EC2 and EBS, you often lose the sophisticated data management, global namespaces, and rich service integrations found in full AWS Regions.

​This is where the combination of Amazon FSx for NetApp ONTAP and Cloud Volumes ONTAP (CVO) transforms the architecture from a "limited edge" to a "limitless data fabric."

The Challenge: Service Scarcity at the Edge
​AWS Local Zones are streamlined by design. They excel at hosting the "hot" part of your application—the frontend or the latency-sensitive processing engine. But data is rarely static. It needs to be backed up, analyzed by AI/ML services in the parent region, or shared across multiple geographical locations.

Common hurdles include:

  • Data Silos: Data trapped in a Local Zone is hard to access for regional services like Amazon SageMaker or AWS Glue.
  • Complexity in Migration: Moving datasets between the edge and the region often requires custom scripts and manual intervention.
  • Limited Protocols: Native Local Zone storage may not offer the multi-protocol support (NFS, SMB, iSCSI) required by enterprise applications. ​ The Solution: NetApp ONTAP as the "Data Highway". ​By deploying Cloud Volumes ONTAP within your Local Zone infrastructure and Amazon FSx for NetApp ONTAP in a standard region, you effectively remove the geographical constraints of your data.
  1. Seamless Data Mobility with SnapMirror
    ​NetApp’s SnapMirror technology allows you to replicate data between a Local Zone and a standard AWS Region (or even on-premises) with extreme efficiency. Instead of "moving" data, you are synchronizing it. This enables a hybrid workflow where:
    Input: Data is captured at the edge (Local Zone) for low-latency processing.
    Transfer: SnapMirror moves only the changed blocks to the Parent Region.
    Output: Regional services (like Redshift or Athena) perform deep analytics on that data without the application ever feeling a performance hit.

  2. Global Accessibility with FlexCache
    ​One of the most powerful features of NetApp ONTAP is FlexCache. Imagine having a "read cache" of your regional dataset sitting right in the Local Zone inside Cloud Volumes ONTAP.
    ​Your "Source of Truth" lives in the full AWS Region inside Amazon FSx for NetApp ONTAP (utilizing lower-cost tiers like S3-backed capacity pools).
    ​Your Local Zone instances access a cache volume that feels like local storage.
    ​If a file is requested at the edge, it’s pulled once, cached, and served at microsecond speeds thereafter. This solves the "data sitting in Local Zones" problem by making regional data local.

  3. Enterprise-Grade Protection at the Edge
    ​Local Zones are often used for regulated industries (Healthcare, Finance) that require strict data residency and protection. ONTAP brings:
    ​Immutable Snapshots: Protect against ransomware at the edge.
    ​Thin Provisioning & Deduplication: Reduce the footprint (and cost) of expensive edge storage.
    ​Multi-protocol Support: Easily migrate "un-migratable" on-premises workloads directly into a Local Zone.

My Perspective: The Future is Distributed, but Unified
​The future of cloud isn't just about moving everything to the "center." It's about building a distributed architecture that functions as a single unit.
​AWS Local Zones provide the muscles (compute) where they are needed most. NetApp ONTAP provides the nervous system (data management), ensuring that information flows seamlessly between the edge and the brain (the Region). If you are building for the edge, don't just think about where your servers are—think about how your data travels.

The goal is simple: High-speed local access, with regional-scale intelligence.

Clean Architecture in .NET 10: Testing What Matters

2026-02-13 13:00:00

Part 7 of 7 — the finale! Start from the beginning if you're new here.

Clean Architecture promises testability. Now let's deliver. We'll write tests that actually catch bugs, skip tests that waste time, and avoid the trap of coverage theater.

What We're Testing

Layer What To Test How
Domain Entities, value objects, business rules Pure unit tests, no mocks
Application Command/query handlers Unit tests with mocked repos
Infrastructure Repositories, DB config Integration tests
API Full request/response cycle Integration tests

Test Projects Setup

mkdir tests
cd tests

# Unit tests
dotnet new xunit -n PromptVault.UnitTests
dotnet add PromptVault.UnitTests reference ../src/PromptVault.Domain
dotnet add PromptVault.UnitTests reference ../src/PromptVault.Application
dotnet add PromptVault.UnitTests package Moq
dotnet add PromptVault.UnitTests package FluentAssertions

# Integration tests
dotnet new xunit -n PromptVault.IntegrationTests
dotnet add PromptVault.IntegrationTests reference ../src/PromptVault.API
dotnet add PromptVault.IntegrationTests package Microsoft.AspNetCore.Mvc.Testing
dotnet add PromptVault.IntegrationTests package FluentAssertions

Domain Tests: Pure and Simple

Domain tests are the easiest. No mocks, no setup—just logic.

tests/PromptVault.UnitTests/Domain/PromptTests.cs

using FluentAssertions;
using PromptVault.Domain.Entities;
using PromptVault.Domain.ValueObjects;

namespace PromptVault.UnitTests.Domain;

public class PromptTests
{
    [Fact]
    public void Constructor_WithValidData_CreatesPromptWithInitialVersion()
    {
        var prompt = new Prompt("My Prompt", "Do something", ModelType.Gpt4);

        prompt.Title.Should().Be("My Prompt");
        prompt.Content.Should().Be("Do something");
        prompt.Versions.Should().HaveCount(1);
        prompt.Versions.First().VersionNumber.Should().Be(1);
    }

    [Theory]
    [InlineData("")]
    [InlineData("   ")]
    [InlineData(null)]
    public void Constructor_WithEmptyTitle_Throws(string? title)
    {
        var act = () => new Prompt(title!, "Content", ModelType.Gpt4);

        act.Should().Throw<ArgumentException>()
            .WithMessage("*Title*required*");
    }

    [Fact]
    public void UpdateContent_WithNewContent_CreatesNewVersion()
    {
        var prompt = new Prompt("Test", "Original", ModelType.Gpt4);

        prompt.UpdateContent("Updated", "[email protected]");

        prompt.Content.Should().Be("Updated");
        prompt.Versions.Should().HaveCount(2);
        prompt.Versions.Last().CreatedBy.Should().Be("[email protected]");
    }

    [Fact]
    public void UpdateContent_WithSameContent_DoesNotCreateVersion()
    {
        var prompt = new Prompt("Test", "Same", ModelType.Gpt4);

        prompt.UpdateContent("Same");

        prompt.Versions.Should().HaveCount(1);
    }

    [Fact]
    public void AddTag_NormalizesAndDeduplicates()
    {
        var prompt = new Prompt("Test", "Content", ModelType.Gpt4);

        prompt.AddTag("Machine Learning");
        prompt.AddTag("machine-learning");  // Same slug
        prompt.AddTag("MACHINE LEARNING");  // Same slug

        prompt.Tags.Should().HaveCount(1);
    }
}

tests/PromptVault.UnitTests/Domain/TagTests.cs

using FluentAssertions;
using PromptVault.Domain.ValueObjects;

namespace PromptVault.UnitTests.Domain;

public class TagTests
{
    [Theory]
    [InlineData("Machine Learning", "machine-learning")]
    [InlineData("AI_Tools", "ai-tools")]
    [InlineData("  spaces  ", "spaces")]
    public void Constructor_NormalizesToSlug(string input, string expectedSlug)
    {
        var tag = new Tag(input);
        tag.Slug.Should().Be(expectedSlug);
    }

    [Fact]
    public void Constructor_WithEmptyValue_Throws()
    {
        var act = () => new Tag("");
        act.Should().Throw<ArgumentException>();
    }

    [Fact]
    public void Constructor_WithTooLongValue_Throws()
    {
        var act = () => new Tag(new string('a', 51));
        act.Should().Throw<ArgumentException>().WithMessage("*50 characters*");
    }
}

No database. No HTTP. No mocking. Just logic and assertions.

Handler Tests: Mocked Dependencies

Handlers are tested with mocked repositories:

tests/PromptVault.UnitTests/Application/CreatePromptCommandHandlerTests.cs

using FluentAssertions;
using Moq;
using PromptVault.Application;
using PromptVault.Application.Commands.CreatePrompt;
using PromptVault.Application.Interfaces;
using PromptVault.Domain.Entities;

namespace PromptVault.UnitTests.Application;

public class CreatePromptCommandHandlerTests
{
    private readonly Mock<IPromptRepository> _repoMock;
    private readonly CreatePromptCommandHandler _handler;

    public CreatePromptCommandHandlerTests()
    {
        _repoMock = new Mock<IPromptRepository>();
        _handler = new CreatePromptCommandHandler(_repoMock.Object);
    }

    [Fact]
    public async Task Handle_WithValidCommand_ReturnsSuccessWithId()
    {
        // Arrange
        _repoMock.Setup(r => r.TitleExistsAsync(It.IsAny<string>(), null, default))
            .ReturnsAsync(false);

        var command = new CreatePromptCommand("Test", "Content", "gpt-4", 
            new List<string> { "tag1" });

        // Act
        var result = await _handler.Handle(command, CancellationToken.None);

        // Assert
        result.IsSuccess.Should().BeTrue();
        result.Value.Should().NotBeEmpty();

        _repoMock.Verify(r => r.AddAsync(
            It.Is<Prompt>(p => p.Title == "Test" && p.Tags.Count == 1), 
            default), Times.Once);
    }

    [Fact]
    public async Task Handle_WithDuplicateTitle_ReturnsConflict()
    {
        _repoMock.Setup(r => r.TitleExistsAsync("Existing", null, default))
            .ReturnsAsync(true);

        var command = new CreatePromptCommand("Existing", "Content", "gpt-4");

        var result = await _handler.Handle(command, CancellationToken.None);

        result.IsSuccess.Should().BeFalse();
        result.ErrorType.Should().Be(ErrorType.Conflict);
        _repoMock.Verify(r => r.AddAsync(It.IsAny<Prompt>(), default), Times.Never);
    }
}

Pattern: Arrange → Act → Assert. Mock the repository, call the handler, verify the result.

Integration Tests: Real HTTP

For integration tests, use WebApplicationFactory:

tests/PromptVault.IntegrationTests/CustomWebApplicationFactory.cs

using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using PromptVault.Infrastructure.Persistence;

namespace PromptVault.IntegrationTests;

public class CustomWebApplicationFactory : WebApplicationFactory<Program>
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            // Remove real DbContext
            var descriptor = services.SingleOrDefault(
                d => d.ServiceType == typeof(DbContextOptions<AppDbContext>));
            if (descriptor != null)
                services.Remove(descriptor);

            // Add in-memory database
            services.AddDbContext<AppDbContext>(options =>
                options.UseInMemoryDatabase("TestDb_" + Guid.NewGuid()));

            // Ensure created
            var sp = services.BuildServiceProvider();
            using var scope = sp.CreateScope();
            var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
            db.Database.EnsureCreated();
        });

        builder.UseEnvironment("Testing");
    }
}

tests/PromptVault.IntegrationTests/PromptsControllerTests.cs

using System.Net;
using System.Net.Http.Json;
using FluentAssertions;
using PromptVault.API.Contracts.Requests;
using PromptVault.API.Contracts.Responses;

namespace PromptVault.IntegrationTests;

public class PromptsControllerTests : IClassFixture<CustomWebApplicationFactory>
{
    private readonly HttpClient _client;

    public PromptsControllerTests(CustomWebApplicationFactory factory)
    {
        _client = factory.CreateClient();
    }

    [Fact]
    public async Task CreatePrompt_WithValidData_ReturnsCreated()
    {
        var request = new CreatePromptRequest(
            $"Test {Guid.NewGuid()}", "Content", "gpt-4", 
            new List<string> { "test" });

        var response = await _client.PostAsJsonAsync("/api/prompts", request);

        response.StatusCode.Should().Be(HttpStatusCode.Created);

        var created = await response.Content.ReadFromJsonAsync<CreatePromptResponse>();
        created!.Id.Should().NotBeEmpty();
        response.Headers.Location.Should().NotBeNull();
    }

    [Fact]
    public async Task CreatePrompt_WithDuplicateTitle_ReturnsConflict()
    {
        var title = $"Duplicate {Guid.NewGuid()}";
        var request = new CreatePromptRequest(title, "Content", "gpt-4");

        await _client.PostAsJsonAsync("/api/prompts", request);
        var response = await _client.PostAsJsonAsync("/api/prompts", request);

        response.StatusCode.Should().Be(HttpStatusCode.Conflict);
    }

    [Fact]
    public async Task GetPrompt_WhenExists_ReturnsOk()
    {
        // Create
        var createReq = new CreatePromptRequest($"Get Test {Guid.NewGuid()}", "Content", "gpt-4");
        var createRes = await _client.PostAsJsonAsync("/api/prompts", createReq);
        var created = await createRes.Content.ReadFromJsonAsync<CreatePromptResponse>();

        // Get
        var response = await _client.GetAsync($"/api/prompts/{created!.Id}");

        response.StatusCode.Should().Be(HttpStatusCode.OK);
        var prompt = await response.Content.ReadFromJsonAsync<PromptResponse>();
        prompt!.Title.Should().Be(createReq.Title);
    }

    [Fact]
    public async Task GetPrompt_WhenNotExists_ReturnsNotFound()
    {
        var response = await _client.GetAsync($"/api/prompts/{Guid.NewGuid()}");
        response.StatusCode.Should().Be(HttpStatusCode.NotFound);
    }

    [Fact]
    public async Task UpdatePrompt_CreatesNewVersion()
    {
        // Create
        var createReq = new CreatePromptRequest($"Version Test {Guid.NewGuid()}", "v1", "gpt-4");
        var createRes = await _client.PostAsJsonAsync("/api/prompts", createReq);
        var created = await createRes.Content.ReadFromJsonAsync<CreatePromptResponse>();

        // Update
        var updateReq = new UpdatePromptRequest(Content: "v2");
        await _client.PutAsJsonAsync($"/api/prompts/{created!.Id}", updateReq);

        // Verify
        var getRes = await _client.GetAsync($"/api/prompts/{created.Id}?includeVersions=true");
        var prompt = await getRes.Content.ReadFromJsonAsync<PromptResponse>();

        prompt!.VersionCount.Should().Be(2);
    }

    [Fact]
    public async Task DeletePrompt_RemovesIt()
    {
        // Create
        var createReq = new CreatePromptRequest($"Delete Test {Guid.NewGuid()}", "Content", "gpt-4");
        var createRes = await _client.PostAsJsonAsync("/api/prompts", createReq);
        var created = await createRes.Content.ReadFromJsonAsync<CreatePromptResponse>();

        // Delete
        var deleteRes = await _client.DeleteAsync($"/api/prompts/{created!.Id}");
        deleteRes.StatusCode.Should().Be(HttpStatusCode.NoContent);

        // Verify gone
        var getRes = await _client.GetAsync($"/api/prompts/{created.Id}");
        getRes.StatusCode.Should().Be(HttpStatusCode.NotFound);
    }
}

What NOT To Test

Testing everything isn't the goal. Skip tests that:

1. Just Test the Framework

// ❌ DON'T TEST THIS
[Fact]
public void DbContext_SaveChanges_Persists()
{
    // You're testing EF Core, not your code
}

2. Are Trivial Mappings

// ❌ DON'T TEST THIS
[Fact]
public void PromptDto_FromEntity_MapsTitle()
{
    // Integration tests will catch this if broken
}

3. Require Excessive Mocking

If you need 10 mocks to test one method, the method does too much. Refactor first.

The Coverage Trap

"We need 80% code coverage!"

Coverage is a terrible metric for test quality. You can have 100% coverage and catch zero bugs:

// ❌ Useless test — 100% coverage, zero value
[Fact]
public void CreatePrompt_Works()
{
    var handler = new CreatePromptCommandHandler(Mock.Of<IPromptRepository>());
    // No assertions. Coverage goes up. Value = zero.
}

Better questions:

  • Do tests fail when you break things?
  • Do tests catch real bugs in PRs?
  • Are tests fast enough that people run them?

Running Tests

# All tests
dotnet test

# Unit tests only
dotnet test tests/PromptVault.UnitTests

# With coverage
dotnet test --collect:"XPlat Code Coverage"

# Specific test
dotnet test --filter "FullyQualifiedName~CreatePromptCommandHandlerTests"

Test Organization

tests/
├── PromptVault.UnitTests/
│   ├── Domain/
│   │   ├── PromptTests.cs
│   │   └── TagTests.cs
│   └── Application/
│       └── CreatePromptCommandHandlerTests.cs
│
└── PromptVault.IntegrationTests/
    ├── CustomWebApplicationFactory.cs
    └── PromptsControllerTests.cs

Key Takeaways

  1. Domain tests are pure — No mocks, no database, just logic
  2. Handler tests mock repositories — Verify coordination logic
  3. Integration tests use real HTTP — Catch wiring issues
  4. Skip trivial tests — Focus on behavior, not coverage
  5. Coverage is a vanity metric — Tests that catch bugs matter

Series Complete! 🎉

Over 7 parts, we built:

  1. Part 1: Solution structure, dependency direction
  2. Part 2: Domain entities with behavior
  3. Part 3: CQRS with MediatR
  4. Part 4: EF Core repositories
  5. Part 5: Thin API controllers
  6. Part 6: Pipeline behaviors
  7. Part 7: Testing at every layer

Final Thoughts

Clean Architecture isn't about perfect circles or rigid folders. It's about:

  • Dependencies point inward — Domain knows nothing about databases
  • Testable by design — Each layer tested in isolation
  • Changeable infrastructure — Swap databases without touching business logic

The ceremony has a cost. For small projects, it's overhead. For large projects with long lifespans, it pays dividends.

Build what you need, not what the architecture diagram shows.

Resources

Get the Code

The complete PromptVault application:

PromptVault

A production-ready .NET 10 API for storing, versioning, and organizing AI prompts.

This is the companion repository for the blog series: Clean Architecture in .NET 10: A Practical Guide

.NET License: MIT Build

What Is This?

PromptVault is a REST API that lets you:

  • 📝 Store prompts with metadata (title, model type, tags)
  • 🔄 Track versions automatically when content changes
  • 📁 Organize into collections (like folders for your prompts)
  • 🔍 Search by content or tags

More importantly, it demonstrates Clean Architecture patterns in a real, runnable application—not just code snippets.

Blog Series

This repo follows along with a 7-part blog series:

Part Topic Branch
0 Introduction: Why Your Code Turns Into Spaghetti main (this branch)
1 The Setup part-1-setup
2 The Domain Layer part-2-domain
3 The Application Layer part-3-application
4 The Infrastructure Layer part-4-infrastructure
5 The API Layer part-5-api
6 Production Polish part-6-production
7 Testing part-7-testing

Each branch represents the state of the…

Clone it. Run it. Make it yours.

Thanks for following along. Now go build something. 🚀

Find the Index of the First Occurrence in a String

2026-02-13 12:47:36

Find the Index of the First Occurrence in a String is a fundamental string-search problem that tests how well you understand pattern matching and boundary handling. You are given two strings: a longer string, often called the “haystack,” and a shorter string, called the “needle.”

Your task is to find the index of the first occurrence of the needle within the haystack. If the needle does not appear in the haystack, you return -1.

Indexing is typically zero-based, meaning the first character of the haystack has index 0. If the needle is an empty string, the expected return value is usually 0, because an empty string is considered to appear at the beginning of any string.

This problem appears frequently in interviews because it looks simple but reveals whether a candidate understands string traversal, edge cases, and efficiency trade-offs.

Why this problem is more than a built-in function

In real programming languages, there is often a built-in method that solves this problem in one line. However, interviews are not about using library calls. They are about understanding what happens underneath.

Interviewers want to see whether you can reason about how strings are compared, how indices are managed, and how to avoid unnecessary work when searching for a pattern.

The straightforward approach and its limits

The most intuitive solution is a sliding window comparison.

You align the needle with the haystack starting at index 0 and compare characters one by one. If all characters match, you return the current index. If a mismatch occurs, you shift the starting position by one and try again.

This process continues until there is no longer enough space left in the haystack for the needle to fit.

This approach is easy to understand and works well for small inputs. It clearly shows your grasp of indexing and loop control, which is why interviewers often accept it as a baseline solution.

Want to explore more coding problem solutions? Check out the Squares of a Sorted Array and Best Time to Buy and Sell Stock with Transaction Fee.

How to reason about correctness

The logic is sound because you only consider valid starting positions.

If the haystack length is n and the needle length is m, there are only n - m + 1 possible positions where the needle could start.

At each position, you check whether all m characters match in sequence. If they do, that is the first occurrence because you scan from left to right.

If no position produces a full match, then the needle does not exist in the haystack.

Time complexity in plain terms

In the worst case, you compare many characters repeatedly. For example, when the haystack contains many repeated characters and the needle almost matches but fails at the last character.

In such cases, the time complexity is proportional to the product of the lengths of the two strings.

For interview constraints, this is usually acceptable unless the problem explicitly asks for optimization.

When interviewers expect more

Some interviewers follow up by asking whether you can do better.

That opens the door to more advanced string-matching algorithms that reduce repeated comparisons by using information about previous mismatches.

These approaches are more complex and are usually expected only if the interviewer explicitly pushes for optimization.

For many roles, being able to clearly explain the straightforward solution, handle edge cases, and reason about complexity is enough.

Common edge cases to handle carefully

One important case is when the needle is longer than the haystack. In that situation, a match is impossible, and the correct return value is -1.

Another is when the needle is an empty string. Most problem definitions specify that the result should be 0.

You should also be careful with index boundaries to avoid reading beyond the end of the haystack.