Back to Blog
6 min read

AI Pair Programming with ChatGPT: A New Development Paradigm

Pair programming has always been about two minds working together. Now we can pair with an AI. Let’s explore how to effectively use ChatGPT as a programming partner.

The AI Pair Programming Experience

Traditional pair programming has a driver (typing) and navigator (reviewing). With ChatGPT:

  • You’re the driver: You maintain control, make decisions, write final code
  • ChatGPT navigates: Suggests approaches, catches issues, explains alternatives

Workflow Example: Building an API

Let’s walk through building a REST API with ChatGPT as our pair.

Step 1: Planning

Me: “I need to build a REST API for a task management system. The requirements are: users can create tasks, assign them to team members, set due dates, and mark them complete. What architecture would you suggest?”

ChatGPT: Suggests Clean Architecture with:

  • Controllers (Presentation)
  • Application services (Business logic)
  • Domain entities
  • Infrastructure (Database, external services)

We discuss tradeoffs and agree on the approach.

Step 2: Domain Modeling

Me: “Let’s start with the domain entities. Here’s my initial Task entity - what do you think?”

public class TaskItem
{
    public Guid Id { get; set; }
    public string Title { get; set; }
    public string Description { get; set; }
    public DateTime? DueDate { get; set; }
    public bool IsComplete { get; set; }
    public Guid AssigneeId { get; set; }
}

ChatGPT: Suggests improvements:

  • Add CreatedAt and UpdatedAt timestamps
  • Use record for immutability
  • Add status enum instead of boolean
  • Consider value objects for Title
  • Add domain events for state changes

We iterate together:

public record TaskItem
{
    public Guid Id { get; init; }
    public TaskTitle Title { get; init; } = null!;
    public string? Description { get; init; }
    public DateTime CreatedAt { get; init; }
    public DateTime? UpdatedAt { get; private set; }
    public DateTime? DueDate { get; init; }
    public TaskStatus Status { get; private set; }
    public Guid? AssigneeId { get; private set; }

    private readonly List<IDomainEvent> _domainEvents = new();
    public IReadOnlyCollection<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();

    public void MarkComplete()
    {
        if (Status == TaskStatus.Completed)
            return;

        Status = TaskStatus.Completed;
        UpdatedAt = DateTime.UtcNow;
        _domainEvents.Add(new TaskCompletedEvent(Id));
    }

    public void AssignTo(Guid userId)
    {
        AssigneeId = userId;
        UpdatedAt = DateTime.UtcNow;
        _domainEvents.Add(new TaskAssignedEvent(Id, userId));
    }
}

public record TaskTitle
{
    public string Value { get; }

    public TaskTitle(string value)
    {
        if (string.IsNullOrWhiteSpace(value))
            throw new ArgumentException("Title cannot be empty", nameof(value));

        if (value.Length > 200)
            throw new ArgumentException("Title cannot exceed 200 characters", nameof(value));

        Value = value;
    }
}

public enum TaskStatus
{
    Pending,
    InProgress,
    Completed,
    Cancelled
}

Step 3: Service Layer

Me: “Now let’s implement the application service. I want to handle task creation with validation.”

We collaborate on the service:

public class TaskService : ITaskService
{
    private readonly ITaskRepository _repository;
    private readonly IUserRepository _userRepository;
    private readonly IEventPublisher _eventPublisher;

    public TaskService(
        ITaskRepository repository,
        IUserRepository userRepository,
        IEventPublisher eventPublisher)
    {
        _repository = repository;
        _userRepository = userRepository;
        _eventPublisher = eventPublisher;
    }

    public async Task<Result<TaskItem>> CreateTaskAsync(
        CreateTaskRequest request,
        CancellationToken cancellationToken)
    {
        // Validate assignee exists if provided
        if (request.AssigneeId.HasValue)
        {
            var assigneeExists = await _userRepository.ExistsAsync(
                request.AssigneeId.Value,
                cancellationToken);

            if (!assigneeExists)
            {
                return Result<TaskItem>.Failure("Assignee not found");
            }
        }

        // Create domain entity
        var task = new TaskItem
        {
            Id = Guid.NewGuid(),
            Title = new TaskTitle(request.Title),
            Description = request.Description,
            DueDate = request.DueDate,
            CreatedAt = DateTime.UtcNow,
            Status = TaskStatus.Pending
        };

        if (request.AssigneeId.HasValue)
        {
            task.AssignTo(request.AssigneeId.Value);
        }

        // Persist
        await _repository.AddAsync(task, cancellationToken);

        // Publish domain events
        foreach (var domainEvent in task.DomainEvents)
        {
            await _eventPublisher.PublishAsync(domainEvent, cancellationToken);
        }

        return Result<TaskItem>.Success(task);
    }
}

Step 4: Debugging Together

Me: “I’m getting a null reference exception on line 42. Here’s the stack trace…”

ChatGPT: Analyzes the code path, identifies that _userRepository might not be properly registered in DI, and suggests:

  1. Check service registration
  2. Add null check with meaningful error
  3. Consider using the null object pattern

Step 5: Testing

Me: “Help me write tests for the CreateTaskAsync method.”

We build comprehensive tests together:

public class TaskServiceTests
{
    private readonly Mock<ITaskRepository> _taskRepoMock;
    private readonly Mock<IUserRepository> _userRepoMock;
    private readonly Mock<IEventPublisher> _eventPublisherMock;
    private readonly TaskService _sut;

    public TaskServiceTests()
    {
        _taskRepoMock = new Mock<ITaskRepository>();
        _userRepoMock = new Mock<IUserRepository>();
        _eventPublisherMock = new Mock<IEventPublisher>();
        _sut = new TaskService(
            _taskRepoMock.Object,
            _userRepoMock.Object,
            _eventPublisherMock.Object);
    }

    [Fact]
    public async Task CreateTaskAsync_WithValidRequest_ReturnsSuccess()
    {
        // Arrange
        var request = new CreateTaskRequest
        {
            Title = "Test Task",
            Description = "Test Description"
        };

        // Act
        var result = await _sut.CreateTaskAsync(request, CancellationToken.None);

        // Assert
        Assert.True(result.IsSuccess);
        Assert.Equal("Test Task", result.Value.Title.Value);
        _taskRepoMock.Verify(r => r.AddAsync(
            It.IsAny<TaskItem>(),
            It.IsAny<CancellationToken>()), Times.Once);
    }

    [Fact]
    public async Task CreateTaskAsync_WithInvalidAssignee_ReturnsFailure()
    {
        // Arrange
        var nonExistentUserId = Guid.NewGuid();
        _userRepoMock.Setup(r => r.ExistsAsync(nonExistentUserId, It.IsAny<CancellationToken>()))
            .ReturnsAsync(false);

        var request = new CreateTaskRequest
        {
            Title = "Test Task",
            AssigneeId = nonExistentUserId
        };

        // Act
        var result = await _sut.CreateTaskAsync(request, CancellationToken.None);

        // Assert
        Assert.False(result.IsSuccess);
        Assert.Contains("not found", result.Error);
    }

    [Theory]
    [InlineData("")]
    [InlineData("   ")]
    [InlineData(null)]
    public async Task CreateTaskAsync_WithEmptyTitle_ThrowsException(string title)
    {
        // Arrange
        var request = new CreateTaskRequest { Title = title };

        // Act & Assert
        await Assert.ThrowsAsync<ArgumentException>(() =>
            _sut.CreateTaskAsync(request, CancellationToken.None));
    }
}

Effective Collaboration Techniques

1. Think Aloud

Share your reasoning: “I’m thinking about using an enum here because…“

2. Ask for Alternatives

“What are other ways to implement this? What are the tradeoffs?“

3. Challenge Suggestions

“Why did you suggest X instead of Y?“

4. Request Explanations

“I don’t understand this part. Can you explain?“

5. Iterative Refinement

“That’s close, but change the error handling to…”

When AI Pair Programming Shines

  • Learning new technologies: The AI explains as you go
  • Boilerplate generation: Let AI handle repetitive code
  • Code review: Fresh perspective on your code
  • Documentation: Generate docs from code
  • Testing: Generate test cases you might miss

When to Take the Lead

  • Architecture decisions: You understand the full context
  • Business logic: Domain knowledge is yours
  • Security: Don’t trust AI with security-critical decisions
  • Performance: Profile and measure, don’t assume

Conclusion

AI pair programming with ChatGPT is a powerful new tool in the developer toolkit. It’s not about replacing human collaboration - it’s about augmenting your capabilities when you’re working solo. The key is learning when to lead and when to follow the AI’s suggestions.

Resources

Michael John Peña

Michael John Peña

Senior Data Engineer based in Sydney. Writing about data, cloud, and technology.