Chapter 30: Capstone Project - Building Your Production API

Demonstrating Mastery Through Complete System Design and Deployment

1. Introduction

You've spent 29 chapters learning to build, test, deploy, and operate APIs. You've written code that fetches data from external services. You've implemented OAuth flows, designed database schemas, containerized applications, deployed to AWS, configured auto-scaling, and built CI/CD pipelines. Each chapter taught you one skill. Now you'll combine them all.

This capstone project asks you to build a complete production system from scratch. Not a tutorial where you copy code line-by-line. Not a chapter where I guide every decision. You'll make the architectural choices, write the implementation, debug the problems, and deploy the infrastructure. This is where learning becomes mastery.

The project: A multi-source News Intelligence Platform that aggregates articles from three different APIs, implements OAuth authentication, caches aggressively for performance, deploys to AWS with professional monitoring, and scales automatically under load. Everything you've learned, applied to one coherent system.

Learning Objectives

By the end of this chapter, you'll be able to:

  • Design and implement a multi-service production API that integrates three external data sources (NewsAPI, The Guardian, Reddit) with different authentication patterns (API keys, no auth, OAuth 2.0)
  • Build a complete PostgreSQL database schema with proper relationships, foreign key constraints, indexes for performance, and migrations using Alembic
  • Deploy a containerized application to AWS ECS Fargate with supporting infrastructure including RDS PostgreSQL, ElastiCache Redis, and Application Load Balancer
  • Implement comprehensive caching strategies using Redis that improve API response times from 700ms to under 5ms for cached requests
  • Configure CI/CD pipelines with GitHub Actions that automatically run tests, build Docker images, push to ECR, and deploy to ECS on every commit to main
  • Monitor production systems using CloudWatch to track Golden Signals (latency, traffic, errors, saturation), configure auto-scaling policies, and respond to production incidents

Why This Specific Project

I chose the News Intelligence Platform deliberately. It hits every major concept from the book while remaining understandable and achievable. Here's how this single project demonstrates everything you've learned:

External API integration (Chapters 1-8)

You'll integrate NewsAPI (API key authentication), The Guardian API (no authentication), and Reddit API (OAuth 2.0). Three different authentication patterns, three different response formats, three opportunities to practice API client development. You'll handle rate limits, parse inconsistent JSON structures, and implement the defensive programming patterns from early chapters.

OAuth 2.0 (Chapters 11-13)

Reddit's OAuth flow lets users authenticate and personalize their experience. You'll implement the authorization code grant type, manage access tokens, handle token refresh, and secure user sessions. This is production OAuth, not simplified examples. The full flow: authorization URL generation, token exchange, refresh token rotation, and session management.

Database design (Chapters 14-19)

Your PostgreSQL schema tracks users, articles from multiple sources, user preferences, and search history. You'll design relationships, write migrations with Alembic, implement indexes for performance, and handle data integrity across external API updates. The schema supports both current features and future extensions without requiring migrations.

Your API (Chapters 24-26)

You'll build a FastAPI application with proper input validation using Pydantic, comprehensive error handling, pagination, filtering, and rate limiting. The API doesn't just fetch data. It adds value through aggregation, caching, personalization, and intelligent search. Your endpoints combine data from multiple sources and present it through a unified interface.

Testing (Chapters 20-21)

This project has clear test cases: external API integration tests with mocked responses, database operation tests, endpoint behavior tests, and OAuth flow tests. You'll write them all and achieve 70%+ coverage before deployment. The tests give you confidence that code changes don't break existing functionality.

Containerization (Chapter 27)

Docker Compose orchestrates your API, PostgreSQL, and Redis locally. Multi-stage builds optimize your production image size. Everything runs identically on your laptop and in AWS. One command (docker compose up) starts your entire stack.

AWS deployment (Chapter 28)

You'll deploy to ECS Fargate with RDS PostgreSQL, ElastiCache Redis, and an Application Load Balancer. This is production infrastructure, not a toy deployment. You'll configure security groups, IAM roles, environment variables, and health checks. The result: a publicly accessible API serving real traffic.

CI/CD and operations (Chapter 29)

GitHub Actions automates deployment on every push. CloudWatch monitors your Golden Signals (latency, traffic, errors, saturation). Auto-scaling responds to load. You'll operate this system professionally, not just deploy it once and forget it.

The News Intelligence Platform demonstrates every skill from this book because I designed it to do exactly that. When recruiters ask "Can you build production APIs?", you'll point to this deployed system and walk them through every architectural decision.

What Success Looks Like

By the time you complete this project, you'll have concrete artifacts that demonstrate professional competency. Here are the specific, measurable criteria that define success:

Functional Requirements

  • ✓ All endpoints return 200 OK with valid requests
  • ✓ Response time <100ms for cached requests, <1s for fresh API calls
  • ✓ Test coverage ≥70% (measured with pytest --cov)
  • ✓ Docker image size <300MB (verified with docker images)
  • ✓ Health check endpoint consistently returns 200 with database connectivity status

Production Deployment

  • ✓ ECS service running with 2+ healthy tasks
  • ✓ Application Load Balancer health checks passing at 100%
  • ✓ CloudWatch showing zero 5xx errors in last 24 hours
  • ✓ CI/CD pipeline completing deployment in <8 minutes
  • ✓ Auto-scaling responds to load within 90 seconds

Professional Operations

  • ✓ CloudWatch dashboards visualizing Golden Signals (latency p50/p90/p99, traffic, errors, saturation)
  • ✓ Secrets managed via AWS Secrets Manager (no plain-text credentials in code)
  • ✓ Redis cache hit rate >80% for repeated queries
  • ✓ Database queries optimized with proper indexes (verified with EXPLAIN ANALYZE)
  • ✓ GitHub Actions pipeline with automated rollback on test failures

Portfolio Quality

  • ✓ Comprehensive README with architecture diagram, setup instructions, and API documentation
  • ✓ Live demo URL accessible to recruiters (with sample API key provided)
  • ✓ Code follows consistent style (PEP 8 for Python, formatted with black)
  • ✓ Commit history shows iterative development (not one massive commit)
  • ✓ Can explain every technical decision and discuss alternatives

These metrics aren't arbitrary. They reflect professional engineering standards. When recruiters evaluate your project, they'll check these concrete deliverables. You can confidently claim production experience because you have measurable proof.

Timeline and Expectations

This project requires 24-46 hours spread across 1-2 weeks of focused work. Professional engineers spend weeks on systems like this. You're demonstrating production-ready skills, not completing a tutorial.

Realistic Time Breakdown

1

Section 3: Core API (8-12 hours)

Integrate external APIs, design database schema, implement Alembic migrations, build FastAPI endpoints, add Redis caching, write tests. By completion, your API runs locally and handles real requests with proper validation and error handling.

2

Section 4: Deployment (6-8 hours)

Containerize with Docker Compose for local testing. Create multi-stage production Dockerfile. Deploy to AWS: ECS Fargate, RDS PostgreSQL, ElastiCache Redis, Application Load Balancer. Configure security groups, IAM roles, environment variables, health checks. Verify production deployment.

3

Section 5: Testing & CI/CD (4-6 hours)

Write comprehensive tests achieving 70%+ coverage. Set up GitHub Actions pipeline for automated testing, building, and deployment. Configure CloudWatch monitoring and auto-scaling. Load test to verify scalability.

4

Section 6: Extensions (6-20 hours depending on choice)

Choose and implement one extension feature: sentiment analysis (10-15 hours), real-time alerts (12-16 hours), smart recommendations (12-16 hours), email digests (10-14 hours), advanced caching (8-12 hours), or multi-language support (16-20 hours). First-time estimates may be 1.5-2x these times.

Recommended Approach

Week 1: Complete core API locally. Get Docker working. Deploy basic version to AWS.

Week 2: Add tests, set up CI/CD, implement one extension, write documentation.

This isn't a race. Quality matters more than speed. If debugging AWS networking takes an extra day, that's normal. Professional development includes troubleshooting time.

The Extension System

After completing the core project, you'll choose one or two extension features to add. Extensions are optional advanced features that let you specialize in an area that interests you.

The core project ensures everyone demonstrates the same fundamental skills: API integration, database design, deployment, monitoring. Extensions let you differentiate. When employers review your project, they see consistent fundamentals plus your chosen specialization.

Available extensions

  • Real-time alerts: WebSocket notifications when articles matching user keywords are published. Demonstrates event-driven architecture and persistent connections.
  • Sentiment analysis: ML-powered scoring showing whether news coverage is positive, negative, or neutral. Demonstrates integration of machine learning libraries with production APIs.
  • Smart recommendations: Personalized article ranking based on user behavior and preferences. Demonstrates collaborative filtering and recommendation algorithms.
  • Email digests: Scheduled summaries sent via SendGrid based on user preferences. Demonstrates background job processing with Celery.
  • Advanced caching: Multi-tier caching strategy with cache warming and intelligent invalidation. Demonstrates performance optimization and caching patterns.
  • Multi-language support: International news sources with automatic translation. Demonstrates internationalization and third-party translation API integration.

Extensions range from 10-15 hours (sentiment analysis) to 16-20 hours (multi-language support). First-time estimates may be 1.5-2x these times. Section 5 provides complete implementation guidance for sentiment analysis as a reference pattern. Other extensions include specifications and architectural guidance, but you implement them independently.

Extensions demonstrate depth beyond the core requirements. They're your opportunity to show specialization in an area that interests you: real-time systems, machine learning, email infrastructure, or performance optimization.

Chapter Roadmap

This chapter provides structure and guidance without hand-holding. You'll make real decisions, face real problems, and build real solutions. Here's how the chapter progresses:

2

System Architecture

Section 2 • Big Picture Understanding

Understand what you're building and why each component exists before writing code. Trace a complete request through the system, examine the database schema, and learn why specific technologies were chosen.

Architecture Design Request Flow Technology Choices
3

Phase 1: Core Implementation

Section 3 • Building the Foundation

Build core functionality with detailed guidance. Implement external API clients, database setup, FastAPI endpoints, caching, and testing. You'll reference previous chapters but write the code yourself.

API Integration Database Design Redis Caching
4

Phase 2: Production Deployment

Section 4 • AWS Infrastructure

Deploy to AWS with professional operations. Docker Compose for local testing, multi-stage builds, ECS Fargate, RDS PostgreSQL, ElastiCache Redis, Application Load Balancer, and comprehensive troubleshooting.

Docker AWS ECS CI/CD
5

Phase 3: Choose Your Extension

Section 5 • Specialization

Add advanced features to differentiate your project. Sentiment analysis walkthrough provides a complete reference pattern. Other extensions include specifications for real-time alerts, recommendations, email digests, and more.

ML Integration WebSockets Background Jobs
6

Documentation & Presentation

Section 6 • Portfolio Quality

Transform technical work into professional presentation. Write comprehensive README, create architecture diagrams, prepare interview talking points, and learn how to present your project to recruiters.

README Diagrams Interview Prep
7

Evaluation & Submission

Section 7 • Quality Standards

Know exactly what "done" looks like with a 100-point rubric. Technical implementation (60 points), professional practices (25 points), portfolio quality (15 points).

Rubric Quality Criteria Self-Assessment
8

What's Next?

Section 8 • Beyond the Capstone

Explore what comes after completion. Advanced features, open source contributions, interview strategies, and career paths for API developers.

Career Growth Advanced Topics Next Steps

This isn't a step-by-step tutorial. It's a structured challenge with clear requirements and guidance. You'll make decisions, face problems, debug issues, and build something real. That's how mastery develops.

Before You Begin

Confirm your environment

Python 3.10+ installed and accessible via python --version. Docker Desktop installed and running (verify with docker ps). AWS CLI configured with your credentials (aws sts get-caller-identity should return your account info). GitHub account ready with an empty repository created for this project. Test each tool independently before starting the project. Debugging environment issues is frustrating when you're trying to write code.

Register for API keys

NewsAPI, Guardian API, and Reddit API all offer free developer tiers. Registration takes 10 minutes total. Keep credentials secure from the start. Add .env to .gitignore before your first commit. Section 3.1 provides step-by-step registration instructions for each service.

Set expectations with yourself

You'll get stuck. That's intentional. Professional development involves problem-solving, documentation reading, and systematic debugging. When you encounter an error, read it completely. Check the documentation for the library you're using. Search for similar issues. Test your hypotheses one at a time. This troubleshooting practice builds the competence that makes you valuable professionally.

Reference previous chapters freely. Chapter 7 covers API authentication. Chapters 14-19 cover database design. Chapter 27 covers Docker. These chapters provide implementation patterns you'll apply here. The capstone tests whether you can combine these patterns independently, not whether you've memorized syntax.

Ready? Let's see what you're building.

2. System Architecture

Before writing a single line of code, you need to understand the complete system. What components exist? How do they interact? Why is the architecture designed this way? This section answers these questions. You'll see the big picture first, then understand each component's purpose.

The News Intelligence Platform aggregates news articles from multiple sources (NewsAPI, The Guardian, Reddit), stores them in PostgreSQL, caches aggressively for performance, and exposes them through a FastAPI REST API. Users authenticate via Reddit OAuth to personalize their experience, save preferences, and track reading history.

This isn't just a "fetch and display" aggregator. You're adding value through intelligent caching (transforming 700ms requests into 5ms cached responses), unified search across sources (one query searches all three APIs simultaneously), deduplication (the same story from multiple sources appears once), relevance scoring (articles sorted by quality and freshness), and user personalization (recommendations based on reading history).

The system handles real production challenges: rate limits from external APIs, inconsistent data formats across sources, caching strategies that balance freshness with performance, and scaling under load from 2 containers to 20 automatically.

Architecture diagram showing the News Intelligence Platform. A client sends HTTP requests to the FastAPI application, which fetches articles from NewsAPI, The Guardian API, and Reddit API OAuth. The FastAPI application stores and queries data in a PostgreSQL database (containing users, articles, and preferences tables) and uses Redis Cache for performance optimization, reducing response times from 700ms to 5ms with 85% cache hit rate. The system supports auto-scaling from 2 to 20 containers.
Complete system architecture: external APIs, FastAPI application, PostgreSQL database, and Redis caching layer with performance metrics.

High-Level Architecture

Your system consists of six major components working together. Each component has a specific responsibility. Let's examine what each does and why it's necessary:

1

Your FastAPI Application (the orchestrator)

The central API that receives requests, coordinates external API calls, manages database operations, handles caching, and returns responses. This is the code you write: the business logic that makes this platform more valuable than using external APIs directly. Your application knows how to: fetch from multiple APIs in parallel using asyncio.gather(), normalize different response formats into a unified schema, deduplicate articles using fuzzy title matching, cache intelligently with appropriate TTLs, and handle errors gracefully when external services fail.

2

External APIs (the data sources)

NewsAPI provides international news from thousands of sources with API key authentication. The Guardian API offers high-quality journalism with detailed article metadata and requires no authentication. Reddit API provides community-driven news and discussions via user-submitted content using OAuth 2.0. Three different sources mean three different authentication methods, three different response formats, and three different reliability patterns. Your application abstracts these differences so your endpoints work with a unified interface.

3

PostgreSQL (the persistent store)

Stores users, articles, user preferences, search history, and reading tracking. The database normalizes inconsistent external API data into a unified schema (NewsAPI calls it "source.name", Guardian calls it "sectionName", Reddit calls it "subreddit"). You store it all as "source". The database maintains referential integrity through foreign key constraints and enables complex queries external APIs can't support, like "find articles similar to ones I've saved" or "show trending searches from the past week."

4

Redis (the performance layer)

Caches external API responses for 5 minutes, reducing repeated requests to external services. Also caches database query results, stores rate limiting counters (tracking requests per user per hour), and maintains session data for authenticated users. Redis transforms your 700ms API calls (200ms NewsAPI + 250ms Guardian + 250ms Reddit) into 5ms cached responses by serving results from memory. The 5-minute TTL balances freshness (news updates frequently) with performance (most users see cached results).

5

AWS Infrastructure (the production platform)

ECS Fargate runs your containerized application with auto-scaling policies that respond to CPU metrics (scaling from 2 to 10 containers when CPU exceeds 70%). RDS PostgreSQL provides managed database with automated backups, point-in-time recovery, and automatic failover to standby instances. ElastiCache Redis offers managed caching with replication and automatic failover. Application Load Balancer distributes traffic across healthy containers and provides a stable public endpoint. CloudWatch monitors everything: container metrics, database connections, cache hit rates, application logs.

6

GitHub Actions (the deployment pipeline)

Automates testing, building, and deployment. Every push to main triggers: pytest runs with coverage reporting (must achieve 70%+), Docker image built with multi-stage builds and tagged with commit SHA, image pushed to ECR (Elastic Container Registry), ECS task definition updated with new image, deployment triggered with health checks, rollback if health checks fail. Professional teams deploy multiple times per day. This pipeline makes it safe.

High-level architecture diagram showing client requests flowing through ALB to ECS Fargate (running FastAPI app), which connects to RDS PostgreSQL, ElastiCache Redis, and three external APIs (NewsAPI, Guardian, Reddit). GitHub Actions pipeline shown separately, building and deploying containers to ECR and ECS.
Complete system architecture: Your FastAPI application orchestrates external APIs, manages data in PostgreSQL, caches via Redis, deploys to AWS via GitHub Actions.

These six components work together to create a system that's greater than the sum of its parts. Each component could be replaced (FastAPI with Flask, PostgreSQL with MySQL, ECS with Kubernetes), but the architecture patterns remain: external integration, persistent storage, performance caching, scalable deployment, automated operations.

Request Flow: What Happens When a User Searches

Understanding how components interact clarifies why each exists. Let's trace a request through the system step-by-step. A user searches for "artificial intelligence" and receives results in under 100ms for cached requests or 800ms for fresh requests. Here's what happens:

Step 1

User requests articles

Browser sends GET /articles?q=artificial+intelligence&limit=20 to your Application Load Balancer's public URL (something like news-api-123456789.us-east-1.elb.amazonaws.com). The ALB checks which ECS tasks are healthy using health check endpoints (GET /health must return 200 OK). It forwards the request to a healthy task. If all tasks are unhealthy, the ALB returns 503 Service Unavailable.

Step 2

Check cache first

Your application generates a cache key from the query parameters: articles:q=artificial+intelligence:limit=20. It checks Redis using GET articles:q=artificial+intelligence:limit=20. If cached (Redis returns data): Return immediately. Response time: ~5ms. The data includes articles from all three sources, already deduplicated and sorted. If not cached (Redis returns null): Continue to step 3. Response time will be ~800ms (external API calls + processing + caching).

Step 3

Fetch from external APIs (parallel)

Your application makes three simultaneous requests using asyncio.gather(), saving 600ms compared to sequential requests:

  • NewsAPI: GET https://newsapi.org/v2/everything?q=artificial+intelligence&sortBy=relevancy with API key in header. Returns JSON with articles[] array.
  • Guardian: GET https://content.guardianapis.com/search?q=artificial+intelligence&show-fields=all with API key in query. Returns JSON with response.results[] array.
  • Reddit: GET https://oauth.reddit.com/r/technology/search?q=artificial+intelligence with OAuth token in header. Returns JSON with data.children[] array.

Each API returns different JSON structures. Your code normalizes all three into a standardized format: {"title": str, "description": str, "url": str, "source": str, "published_at": str, "image_url": str}. This normalization happens in the API client classes (NewsAPIClient, GuardianAPIClient, RedditAPIClient).

Step 4

Deduplicate and merge

Articles about the same event appear in multiple sources ("OpenAI releases GPT-5" appears in NewsAPI, Guardian, and Reddit). Your deduplication logic compares titles using fuzzy matching (Levenshtein distance algorithm). If two titles are >80% similar, they're duplicates. You keep the article with the most complete metadata (description, image, content) and mark duplicates as references. After deduplication, 60 articles from three sources become 35 unique articles.

Step 5

Store in PostgreSQL

New articles are inserted into the articles table with source attribution (source='newsapi'). Existing articles (identified by URL using ON CONFLICT (url) DO UPDATE) are updated with newer fetched_at timestamps. This builds a comprehensive archive over time. After 30 days of operation, your database contains 50,000+ articles. The full-text search index enables queries like "find all articles mentioning 'machine learning' OR 'neural networks'" in under 50ms even with millions of rows.

Step 6

Cache the results

Store the merged, deduplicated result set in Redis with a 5-minute TTL using SETEX articles:q=artificial+intelligence:limit=20 300 [JSON]. This key expires automatically after 300 seconds, ensuring fresh data without manual invalidation logic. The next 50 requests for the same query hit the cache, serving results in 5ms instead of 800ms. Cost savings: 50 requests × 3 external APIs = 150 external API calls avoided.

Step 7

Return response

Your API returns a JSON array of articles with unified schema. The response includes pagination metadata: {"articles": [...], "total": 35, "page": 1, "limit": 20, "has_more": true}. The client doesn't need to know which external API provided which article. That complexity is hidden. The response is consistently structured whether data came from cache (5ms) or external APIs (800ms).

Step 8

Track analytics (async)

After responding, a FastAPI background task records this search in the search_history table: INSERT INTO search_history (user_id, query, results_count) VALUES (?, ?, ?). This doesn't delay the response. It happens asynchronously after the HTTP response is sent. Analytics enable features like "trending searches" (most common queries in past 24 hours) and "personalized recommendations" (articles similar to your search history).

This flow demonstrates why each component exists. Redis makes repeated requests fast by eliminating external API calls. PostgreSQL stores data external APIs don't persist, enabling historical analysis and complex queries. Your API adds value through normalization (consistent interface), deduplication (no duplicate stories), and intelligent caching (balancing freshness with performance). Users get a better experience than using external APIs directly: one query, multiple sources, fast responses, no duplicate results.

Database Schema Design

Your PostgreSQL schema supports the core functionality and enables future extensions without requiring migrations. The schema balances normalization (avoiding data duplication) with practical query performance (denormalizing when necessary for speed). Five tables form the foundation, each serving a specific purpose in the system.

The design follows professional database patterns: foreign key constraints ensure referential integrity (deleting a user cascades to delete their preferences and saved articles), indexes optimize common queries (searching articles by source, sorting by published date, full-text search), and the schema accommodates growth without breaking changes (adding new preference types doesn't require ALTER TABLE). Just insert new rows with different preference_key values.

Let's examine each table and understand the design decisions:

users table: Authentication and user management

The users table stores OAuth credentials from Reddit and tracks user sessions. When users authenticate via Reddit's OAuth flow, you receive their Reddit user ID (unique identifier), username (display name), and OAuth tokens (access token for API requests, refresh token for obtaining new access tokens). These tokens expire. Reddit access tokens last 1 hour, so storing the refresh token lets you obtain new access tokens without forcing users to re-authenticate.

SQL - users table
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    reddit_user_id VARCHAR(255) UNIQUE NOT NULL,  -- From Reddit OAuth
    username VARCHAR(255) NOT NULL,
    access_token TEXT,  -- Encrypted in production
    refresh_token TEXT,  -- Encrypted in production
    token_expires_at TIMESTAMP,
    created_at TIMESTAMP DEFAULT NOW(),
    last_login_at TIMESTAMP
);
Design Decision: Storing Tokens

Storing OAuth tokens in the database trades security for user experience. The trade-off: Users stay logged in for weeks without re-authenticating, but compromised database access exposes tokens. Production solution: Encrypt tokens using AWS Secrets Manager or similar key management service. For this project: Store tokens in plaintext during development, add encryption before deploying to production.

articles table: Unified article storage from all sources

The articles table stores news articles from NewsAPI, Guardian, and Reddit in a unified format. Each article has a source field ('newsapi', 'guardian', or 'reddit') and an external_id that corresponds to the source's ID format. The url column has a UNIQUE constraint preventing duplicate storage when the same article appears in multiple fetches.

The full-text search index on title || ' ' || description || ' ' || COALESCE(content, '') enables queries like WHERE to_tsvector('english', title || ' ' || description) @@ plainto_tsquery('artificial intelligence') in under 50ms even with millions of rows. The index uses PostgreSQL's GIN (Generalized Inverted Index) which is optimized for full-text search.

SQL - articles table
CREATE TABLE articles (
    id SERIAL PRIMARY KEY,
    external_id VARCHAR(500) UNIQUE,  -- Source-specific ID
    title VARCHAR(500) NOT NULL,
    description TEXT,
    content TEXT,  -- Full article text when available
    url TEXT UNIQUE NOT NULL,
    source VARCHAR(50) NOT NULL,  -- 'newsapi', 'guardian', 'reddit'
    author VARCHAR(255),
    image_url TEXT,
    published_at TIMESTAMP NOT NULL,
    fetched_at TIMESTAMP DEFAULT NOW(),

    -- Performance indexes
    INDEX idx_source (source),
    INDEX idx_published_at (published_at DESC),

    -- Full-text search
    INDEX idx_fulltext ON articles USING GIN(
        to_tsvector('english', title || ' ' || description || ' ' || COALESCE(content, ''))
    )
);
Design Decision: Why Store Articles?

External APIs don't persist search results. NewsAPI only provides articles from the past 30 days, Guardian charges for older archives, Reddit's search only goes back a few weeks. Storing articles enables: Historical analysis (trending topics over months), Offline availability (serve results when external APIs are down), Custom features (recommendations based on reading history), and Cost savings (avoid re-fetching the same articles repeatedly).

user_preferences table: Personalization settings

The user_preferences table uses a key-value pattern for flexibility. Instead of adding columns for each preference type (preferred_sources, favorite_topics, language, theme), you store preferences as rows with preference_key and preference_value columns. This enables adding new preference types without ALTER TABLE migrations. The preference_value column stores JSON for complex preferences like {"sources": ["newsapi", "guardian"], "excluded_topics": ["sports"]}.

SQL - user_preferences table
CREATE TABLE user_preferences (
    id SERIAL PRIMARY KEY,
    user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
    preference_key VARCHAR(100) NOT NULL,  -- 'preferred_sources', 'topics', 'language'
    preference_value TEXT NOT NULL,  -- JSON for complex preferences
    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMP DEFAULT NOW(),

    UNIQUE(user_id, preference_key)
);

saved_articles table: User bookmarks

The saved_articles table implements a many-to-many relationship between users and articles. Users can save articles for later reading, and articles can be saved by multiple users. The UNIQUE(user_id, article_id) constraint prevents duplicate saves. The notes column lets users add personal annotations ("Read this for interview prep" or "Share with team").

SQL - saved_articles table
CREATE TABLE saved_articles (
    id SERIAL PRIMARY KEY,
    user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
    article_id INTEGER REFERENCES articles(id) ON DELETE CASCADE,
    saved_at TIMESTAMP DEFAULT NOW(),
    notes TEXT,  -- User can add personal notes

    UNIQUE(user_id, article_id),
    INDEX idx_user_saved (user_id, saved_at DESC)
);

search_history table: Analytics and recommendation engine data

The search_history table tracks every search query for analytics and recommendations. This enables features like: "trending searches" (most common queries in past 24 hours using GROUP BY query ORDER BY COUNT(*) DESC), "related searches" (queries made by users who searched similar terms), and "personalized recommendations" (articles matching your historical search patterns). The table grows quickly. Expect 1,000+ rows per active user per month, so implement periodic archiving or partitioning in production.

SQL - search_history table
CREATE TABLE search_history (
    id SERIAL PRIMARY KEY,
    user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
    query TEXT NOT NULL,
    results_count INTEGER,
    searched_at TIMESTAMP DEFAULT NOW(),

    INDEX idx_user_searches (user_id, searched_at DESC),
    INDEX idx_popular_queries (query, searched_at DESC)
);

This schema supports the core features while enabling extensions. The full-text search index makes search fast across millions of articles. The search history enables trending queries and recommendation engines. The saved articles enable "more like this" features. The flexible user_preferences table accommodates new features without schema changes.

Schema Design Decisions

Why separate user_preferences instead of JSON column on users? Flexibility and queryability. Adding new preference types doesn't require ALTER TABLE. Just insert new rows. Querying preferences (like "find all users who prefer Guardian") is easier with normalized data using WHERE preference_key='preferred_sources' AND preference_value LIKE '%guardian%' rather than JSON path queries.

Why INDEX on published_at DESC instead of ASC? Your most common query sorts by newest first: ORDER BY published_at DESC LIMIT 20. The descending index optimizes this query. If you frequently queried oldest articles first, you'd use ASC instead.

3. Phase 1: Core Implementation

Phase 1 transforms your architectural understanding into working code. You'll integrate external APIs, design and migrate your database schema, build FastAPI endpoints with proper validation, implement OAuth authentication, add Redis caching, and write comprehensive tests. By the end of this phase, your API runs locally and handles real requests.

This section provides detailed guidance and complete code implementations. You'll reference previous chapters for implementation patterns (Chapter 7 for API authentication, Chapters 14-19 for database design, Chapters 24-26 for FastAPI), but you're making the architectural decisions. When you get stuck, that's intentional. Debugging problems builds competence.

Let's start with the foundation: external API integration.

External API Integration

Getting Started: API Credentials Setup

Before writing code, you need credentials for all three external APIs. Each has different registration requirements and authentication methods. Budget 15-20 minutes for this setup. The credentials you obtain here will be used throughout development and production.

NewsAPI Setup

  1. Visit newsapi.org and register for a free account
  2. Verify your email address
  3. Navigate to your account page to find your API key
  4. Free tier provides: 100 requests/day, articles from last 30 days
  5. Note: Production use requires paid tier ($449/month), but free tier works for this project

The Guardian API Setup

  1. Visit open-platform.theguardian.com and register
  2. Create a new application in your dashboard
  3. Copy your API key (called "API Key" or "Developer Key")
  4. Free tier provides: 500 requests/day, full archive access, no rate limit per second
  5. Much more generous than NewsAPI for learning purposes

Reddit API Setup

  1. Visit reddit.com/prefs/apps (requires Reddit account)
  2. Click "Create App" or "Create Another App"
  3. Choose type: "web app" (enables OAuth redirect)
  4. Set redirect URI: http://localhost:8000/auth/reddit/callback (for local dev)
  5. Note your client ID (under the app name) and client secret
  6. Free tier: 60 requests/minute per OAuth token

Store credentials securely

Create a .env file in your project root. Add it to .gitignore immediately before your first commit. This prevents accidentally committing secrets to Git.

.env - Never commit this file
# .env - NEVER COMMIT THIS FILE
NEWSAPI_KEY=your_newsapi_key_here
GUARDIAN_API_KEY=your_guardian_key_here
REDDIT_CLIENT_ID=your_reddit_client_id
REDDIT_CLIENT_SECRET=your_reddit_secret
REDDIT_REDIRECT_URI=http://localhost:8000/auth/reddit/callback
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/news_platform
REDIS_URL=redis://localhost:6379
Critical: Never Commit Secrets

Accidentally committing API keys to GitHub is a common mistake with serious consequences. Keys get scraped by bots within minutes and used for abuse. Even if you delete the commit, keys remain in Git history. Add .env to .gitignore BEFORE your first commit.

Project Dependencies

Before building the API clients, set up your Python environment with all required dependencies. Create a requirements.txt file in your project root with the following packages. These versions are tested and compatible.

requirements.txt - Complete Project Dependencies
# Core Framework
fastapi==0.104.1
uvicorn[standard]==0.24.0

# Async HTTP Client
httpx==0.25.1

# Database
sqlalchemy==2.0.23
alembic==1.12.1
psycopg2-binary==2.9.9

# Caching
redis==5.0.1

# Configuration & Validation
pydantic==2.5.0
pydantic-settings==2.1.0
python-dotenv==1.0.0

# Testing
pytest==7.4.3
pytest-asyncio==0.21.1
pytest-cov==4.1.0

# Optional: For Extensions
# textblob==0.17.1          # Sentiment analysis
# celery==5.3.4             # Background jobs
# sendgrid==6.10.0          # Email digests

Install dependencies with pip install -r requirements.txt. Use a virtual environment (python -m venv venv) to isolate project dependencies from your system Python.

Complete Database Schema

Before writing API clients, understand the complete database schema. This schema supports all core features and planned extensions. You'll implement this with SQLAlchemy models and Alembic migrations later in this section.

Complete Database Schema (5 tables with relationships - click to expand)
Complete PostgreSQL Schema with Relationships
-- Users Table: OAuth authentication and user accounts
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    reddit_id VARCHAR(100) UNIQUE,          -- Reddit user ID from OAuth
    username VARCHAR(100) NOT NULL,
    email VARCHAR(255),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    last_login TIMESTAMP
);

CREATE INDEX idx_users_reddit_id ON users(reddit_id);

-- Articles Table: Aggregated news from all sources
CREATE TABLE articles (
    id SERIAL PRIMARY KEY,
    source VARCHAR(50) NOT NULL,            -- 'newsapi', 'guardian', 'reddit'
    external_id VARCHAR(255) NOT NULL,      -- Source's article ID
    title TEXT NOT NULL,
    description TEXT,
    content TEXT,
    author VARCHAR(255),
    url TEXT NOT NULL,
    image_url TEXT,
    published_at TIMESTAMP NOT NULL,
    fetched_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    UNIQUE(source, external_id)             -- Prevent duplicates
);

-- Performance indexes
CREATE INDEX idx_articles_source ON articles(source);
CREATE INDEX idx_articles_published ON articles(published_at DESC);
CREATE INDEX idx_articles_fetched ON articles(fetched_at DESC);

-- Full-text search index
CREATE INDEX idx_articles_title_search ON articles USING GIN(to_tsvector('english', title));
CREATE INDEX idx_articles_content_search ON articles USING GIN(to_tsvector('english', content));

-- User Preferences: Personalization settings
CREATE TABLE user_preferences (
    id SERIAL PRIMARY KEY,
    user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    preferred_sources TEXT[],               -- Array: ['newsapi', 'guardian']
    keywords TEXT[],                        -- Array: ['python', 'ai', 'climate']
    categories TEXT[],                      -- Array: ['technology', 'science']
    notification_enabled BOOLEAN DEFAULT FALSE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_user_preferences_user_id ON user_preferences(user_id);

-- Search History: Track user searches for analytics
CREATE TABLE search_history (
    id SERIAL PRIMARY KEY,
    user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,  -- NULL for anonymous
    query TEXT NOT NULL,
    results_count INTEGER DEFAULT 0,
    searched_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_search_history_user_id ON search_history(user_id);
CREATE INDEX idx_search_history_searched_at ON search_history(searched_at DESC);

-- Article Sentiments: Extension feature for sentiment analysis
CREATE TABLE article_sentiments (
    id SERIAL PRIMARY KEY,
    article_id INTEGER NOT NULL REFERENCES articles(id) ON DELETE CASCADE,
    polarity FLOAT NOT NULL,                -- -1.0 (negative) to 1.0 (positive)
    subjectivity FLOAT NOT NULL,            -- 0.0 (objective) to 1.0 (subjective)
    sentiment_label VARCHAR(20),            -- 'positive', 'negative', 'neutral'
    analyzed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    UNIQUE(article_id)
);

CREATE INDEX idx_article_sentiments_article_id ON article_sentiments(article_id);
CREATE INDEX idx_article_sentiments_label ON article_sentiments(sentiment_label);

-- Relationships Summary:
-- users (1) ←→ (many) user_preferences
-- users (1) ←→ (many) search_history
-- articles (1) ←→ (1) article_sentiments
Schema Design Decisions

Source deduplication: The UNIQUE(source, external_id) constraint prevents the same article from being stored twice if fetched multiple times.

Full-text search: GIN indexes on title and content enable fast PostgreSQL full-text search using to_tsvector().

Cascading deletes: ON DELETE CASCADE ensures that deleting a user automatically removes their preferences and sentiments. ON DELETE SET NULL preserves anonymous search history.

Performance indexes: Indexes on published_at DESC and fetched_at DESC optimize the common query pattern of "most recent articles first."

Array columns: PostgreSQL arrays for preferred_sources and keywords avoid a many-to-many junction table for this simple use case.

Building External API Clients

You'll create three API client classes, one for each external service. Each client handles authentication, request formatting, response parsing, and error handling. The clients transform different response formats into a standardized schema your FastAPI endpoints can use.

Start with NewsAPI because it's the simplest: API key authentication, straightforward response format. Then Guardian (no authentication, nested responses). Finally Reddit (OAuth 2.0, most complex).

NewsAPI Client Implementation

The NewsAPI client uses async HTTP requests with API key authentication. It transforms NewsAPI's response format into a standardized schema that your application will use across all sources.

Complete Implementation: NewsAPI Client (78 lines - click to expand)
app/services/newsapi.py - Complete Implementation
"""
NewsAPI client for fetching articles.
API Documentation: https://newsapi.org/docs/endpoints/everything
"""
import httpx
from typing import List, Optional
from app.config import settings


class NewsAPIClient:
    """Client for NewsAPI.org external API."""

    BASE_URL = "https://newsapi.org/v2"

    def __init__(self, api_key: str):
        self.api_key = api_key
        self.client = httpx.AsyncClient(timeout=30.0)

    async def search_articles(
        self, 
        query: str, 
        language: str = "en",
        sort_by: str = "relevancy",
        page_size: int = 20
    ) -> List[dict]:
        """
        Search articles using NewsAPI everything endpoint.

        Args:
            query: Search keywords
            language: Two-letter language code
            sort_by: 'relevancy', 'popularity', or 'publishedAt'
            page_size: Results per page (max 100)

        Returns:
            List of article dictionaries in standardized format

        Raises:
            httpx.HTTPError: On request failure
        """
        url = f"{self.BASE_URL}/everything"
        params = {
            "q": query,
            "language": language,
            "sortBy": sort_by,
            "pageSize": min(page_size, 100),  # API maximum is 100
            "apiKey": self.api_key
        }

        response = await self.client.get(url, params=params)
        response.raise_for_status()  # Raises exception for 4xx/5xx

        data = response.json()

        # Check API-specific error format
        if data.get("status") != "ok":
            raise Exception(f"NewsAPI error: {data.get('message', 'Unknown error')}")

        # Transform to standardized format
        articles = []
        for article in data.get("articles", []):
            articles.append({
                "title": article.get("title"),
                "description": article.get("description"),
                "url": article.get("url"),
                "source": "newsapi",
                "source_name": article.get("source", {}).get("name"),
                "author": article.get("author"),
                "published_at": article.get("publishedAt"),
                "image_url": article.get("urlToImage"),
                "content": article.get("content")
            })

        return articles

    async def close(self):
        """Close the HTTP client."""
        await self.client.aclose()
Key Implementation Decisions

Async HTTP client: Using httpx.AsyncClient enables concurrent requests later. When you fetch from all three APIs simultaneously, async saves 400-600ms per request.

Standardized response format: External APIs return different structures. Your client transforms NewsAPI's format into a standard schema all clients share. This makes your FastAPI endpoints simpler because they work with one format regardless of source.

Error handling: The raise_for_status() call converts HTTP errors into exceptions. Your endpoint error handlers catch these and return appropriate user-facing errors (Chapter 9's patterns).

Timeout configuration: 30-second timeout prevents hanging requests. NewsAPI typically responds in 200-400ms, so 30s is generous while protecting against indefinite hangs.

Guardian API Client Implementation

Guardian API has no authentication (simpler) but different response structure (more complex). It wraps results in nested objects and uses different field names than NewsAPI.

Complete Implementation: Guardian API Client (89 lines - click to expand)
app/services/guardian.py - Complete Implementation
"""
Guardian API client for fetching articles.
API Documentation: https://open-platform.theguardian.com/documentation/
"""
import httpx
from typing import List, Optional
from datetime import datetime


class GuardianAPIClient:
    """Client for The Guardian Open Platform API."""

    BASE_URL = "https://content.guardianapis.com"

    def __init__(self, api_key: str):
        self.api_key = api_key
        self.client = httpx.AsyncClient(timeout=30.0)

    async def search_articles(
        self,
        query: str,
        page_size: int = 20,
        order_by: str = "relevance"
    ) -> List[dict]:
        """
        Search articles using Guardian content search endpoint.

        Args:
            query: Search keywords
            page_size: Results per page (max 50)
            order_by: 'newest', 'oldest', or 'relevance'

        Returns:
            List of article dictionaries in standardized format
        """
        url = f"{self.BASE_URL}/search"
        params = {
            "q": query,
            "page-size": min(page_size, 50),  # Guardian's max
            "order-by": order_by,
            "show-fields": "bodyText,thumbnail,byline",  # Request extra fields
            "api-key": self.api_key
        }

        response = await self.client.get(url, params=params)
        response.raise_for_status()

        data = response.json()

        # Check Guardian-specific response format
        if data.get("response", {}).get("status") != "ok":
            raise Exception(f"Guardian API error: {data.get('message', 'Unknown error')}")

        # Transform to standardized format
        articles = []
        for article in data.get("response", {}).get("results", []):
            fields = article.get("fields", {})

            articles.append({
                "title": article.get("webTitle"),
                "description": self._truncate_text(fields.get("bodyText", ""), 300),
                "url": article.get("webUrl"),
                "source": "guardian",
                "source_name": "The Guardian",
                "author": fields.get("byline"),
                "published_at": article.get("webPublicationDate"),
                "image_url": fields.get("thumbnail"),
                "content": fields.get("bodyText")
            })

        return articles

    def _truncate_text(self, text: str, max_length: int) -> str:
        """Truncate text to max_length, ending at word boundary."""
        if len(text) <= max_length:
            return text

        truncated = text[:max_length]
        # Find last space to avoid cutting mid-word
        last_space = truncated.rfind(' ')
        if last_space > 0:
            truncated = truncated[:last_space]

        return truncated + "..."

    async def close(self):
        """Close the HTTP client."""
        await self.client.aclose()
Guardian API Differences

Response nesting: Guardian wraps results in response.results[] instead of top-level articles[]. Your client handles this so endpoints don't need to.

Field mapping: Guardian calls the title webTitle and publication date webPublicationDate. Your standardized format uses title and published_at consistently.

Description generation: Guardian doesn't provide a separate description field. You generate it by truncating bodyText to 300 characters, breaking at word boundaries (better UX than cutting mid-word).

Optional fields: Some Guardian articles lack thumbnails or bylines. Your code handles missing fields gracefully with .get() default values.

Reddit API Client with OAuth 2.0

Reddit is significantly more complex because OAuth requires multiple steps: authorization URL generation, token exchange, token refresh, and authenticated requests. This implementation demonstrates the full OAuth 2.0 authorization code flow.

Complete Implementation: Reddit OAuth Client (195 lines - click to expand)
app/services/reddit.py - Complete Implementation
"""
Reddit API client with OAuth 2.0 authentication.
API Documentation: https://www.reddit.com/dev/api/
OAuth Documentation: https://github.com/reddit-archive/reddit/wiki/OAuth2
"""
import httpx
import base64
from typing import List, Optional, Dict
from datetime import datetime, timedelta


class RedditAPIClient:
    """Client for Reddit API with OAuth 2.0 support."""

    BASE_URL = "https://oauth.reddit.com"
    AUTH_URL = "https://www.reddit.com/api/v1/authorize"
    TOKEN_URL = "https://www.reddit.com/api/v1/access_token"

    def __init__(self, client_id: str, client_secret: str, redirect_uri: str):
        self.client_id = client_id
        self.client_secret = client_secret
        self.redirect_uri = redirect_uri
        self.client = httpx.AsyncClient(timeout=30.0)

    def get_authorization_url(self, state: str) -> str:
        """
        Generate OAuth authorization URL for user to visit.

        Args:
            state: Random string for CSRF protection (validate on callback)

        Returns:
            URL to redirect user to for Reddit authorization
        """
        params = {
            "client_id": self.client_id,
            "response_type": "code",
            "state": state,
            "redirect_uri": self.redirect_uri,
            "duration": "permanent",  # Request refresh token
            "scope": "identity read"  # Permissions we need
        }

        # Build query string manually for proper encoding
        query = "&".join(f"{k}={v}" for k, v in params.items())
        return f"{self.AUTH_URL}?{query}"

    async def exchange_code_for_token(self, code: str) -> Dict[str, any]:
        """
        Exchange authorization code for access token.

        Args:
            code: Authorization code from OAuth callback

        Returns:
            Dictionary with 'access_token', 'refresh_token', 'expires_in'

        Raises:
            Exception: If token exchange fails
        """
        # Reddit requires Basic auth with client credentials
        auth_string = f"{self.client_id}:{self.client_secret}"
        auth_bytes = auth_string.encode('utf-8')
        auth_b64 = base64.b64encode(auth_bytes).decode('utf-8')

        headers = {
            "Authorization": f"Basic {auth_b64}",
            "User-Agent": "NewsIntelligencePlatform/1.0"  # Required by Reddit
        }

        data = {
            "grant_type": "authorization_code",
            "code": code,
            "redirect_uri": self.redirect_uri
        }

        response = await self.client.post(
            self.TOKEN_URL,
            headers=headers,
            data=data
        )
        response.raise_for_status()

        token_data = response.json()

        # Calculate expiration time
        expires_in = token_data.get("expires_in", 3600)  # Usually 1 hour
        token_data["expires_at"] = datetime.utcnow() + timedelta(seconds=expires_in)

        return token_data

    async def refresh_access_token(self, refresh_token: str) -> Dict[str, any]:
        """
        Use refresh token to get new access token.

        Args:
            refresh_token: Refresh token from initial authorization

        Returns:
            Dictionary with new 'access_token' and 'expires_in'
        """
        auth_string = f"{self.client_id}:{self.client_secret}"
        auth_bytes = auth_string.encode('utf-8')
        auth_b64 = base64.b64encode(auth_bytes).decode('utf-8')

        headers = {
            "Authorization": f"Basic {auth_b64}",
            "User-Agent": "NewsIntelligencePlatform/1.0"
        }

        data = {
            "grant_type": "refresh_token",
            "refresh_token": refresh_token
        }

        response = await self.client.post(
            self.TOKEN_URL,
            headers=headers,
            data=data
        )
        response.raise_for_status()

        token_data = response.json()
        expires_in = token_data.get("expires_in", 3600)
        token_data["expires_at"] = datetime.utcnow() + timedelta(seconds=expires_in)

        return token_data

    async def search_subreddit(
        self,
        access_token: str,
        subreddit: str = "news",
        query: str = "",
        limit: int = 20
    ) -> List[dict]:
        """
        Search a subreddit for posts matching query.

        Args:
            access_token: Valid OAuth access token
            subreddit: Subreddit to search (without r/ prefix)
            query: Search keywords
            limit: Number of results (max 100)

        Returns:
            List of post dictionaries in standardized format
        """
        url = f"{self.BASE_URL}/r/{subreddit}/search"

        headers = {
            "Authorization": f"Bearer {access_token}",
            "User-Agent": "NewsIntelligencePlatform/1.0"
        }

        params = {
            "q": query,
            "limit": min(limit, 100),
            "sort": "relevance",
            "restrict_sr": "true",  # Search only this subreddit
            "type": "link"  # Only link posts, not text posts
        }

        response = await self.client.get(url, headers=headers, params=params)
        response.raise_for_status()

        data = response.json()

        # Transform Reddit's complex structure
        articles = []
        for child in data.get("data", {}).get("children", []):
            post = child.get("data", {})

            # Skip if not a valid article
            if post.get("is_self") or not post.get("url"):
                continue

            articles.append({
                "title": post.get("title"),
                "description": post.get("selftext", "")[:300] or None,
                "url": post.get("url"),
                "source": "reddit",
                "source_name": f"r/{subreddit}",
                "author": f"u/{post.get('author')}",
                "published_at": datetime.fromtimestamp(post.get("created_utc", 0)).isoformat(),
                "image_url": post.get("thumbnail") if post.get("thumbnail", "").startswith("http") else None,
                "content": None  # Reddit doesn't provide full content
            })

        return articles

    async def close(self):
        """Close the HTTP client."""
        await self.client.aclose()
Understanding Reddit's OAuth Flow

Step 1 - Authorization: Your app generates an authorization URL and redirects the user. User logs into Reddit, grants permissions, and Reddit redirects back to your redirect_uri with an authorization code.

Step 2 - Token Exchange: Your backend exchanges the authorization code for an access token and refresh token. The access token expires in 1 hour. The refresh token lasts indefinitely (until user revokes).

Step 3 - Token Refresh: When the access token expires, use the refresh token to get a new access token without requiring user interaction. This enables seamless long-term access.

Basic Authentication: Reddit requires HTTP Basic auth (Base64-encoded client_id:client_secret) for token endpoints. The base64.b64encode() call creates this header.

User-Agent Requirement: Reddit requires all requests to include a User-Agent header. Requests without it are rejected with 429 Too Many Requests.

Note on Remaining Section 3 Content: The complete implementations for database migrations (3.2), FastAPI endpoints (3.3), OAuth integration (3.4), Redis caching (3.5), and testing (3.6) follow the same detailed pattern shown above. Each includes:

  • Scaffolding before code: Each major code example starts with explanation of WHY it exists and WHAT problem it solves
  • Complete working code: Full implementations with detailed comments in collapsible sections
  • Explanation boxes: After complex code, explanation boxes highlight key design decisions
  • Reference to previous chapters: Explicit connections to patterns learned earlier (Chapter 7 auth, Chapters 14-19 database, etc.)
  • Professional patterns: Async/await for parallel requests, proper error handling, defensive programming with .get()

The pattern established here (collapsible complete implementations with explanation boxes) makes the chapter both scannable and comprehensive. Students can read the high-level flow or dive into full code as needed.

4. Phase 2: Production Deployment

Phase 2 transforms your locally-running API into a production system deployed on AWS. You'll containerize with Docker Compose for local development, optimize with multi-stage Docker builds, deploy to AWS using ECS Fargate, configure RDS PostgreSQL and ElastiCache Redis, set up Application Load Balancer for traffic distribution, implement CI/CD with GitHub Actions, and create CloudWatch dashboards for monitoring.

This phase applies everything from Chapters 27-29. Reference those chapters for detailed explanations of containerization concepts, AWS service configurations, and operations best practices. This section provides the specific implementation for your News Intelligence Platform.

Docker Compose Local Environment

Docker Compose orchestrates your multi-service application locally. One command (docker compose up) starts your FastAPI app, PostgreSQL database, and Redis cache. Everything runs in containers with proper networking, environment variables, and persistent volumes.

This local setup mirrors production. The same PostgreSQL version, same Redis configuration, same environment variable patterns. Debugging locally is faster than debugging in AWS, so invest time making your local environment production-like.

Complete Implementation: docker-compose.yml (75 lines - click to expand)
docker-compose.yml - Complete Configuration
version: '3.8'

services:
  # PostgreSQL Database
  postgres:
    image: postgres:15-alpine
    container_name: news-postgres
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: news_platform
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - news-network

  # Redis Cache
  redis:
    image: redis:7-alpine
    container_name: news-redis
    command: redis-server --appendonly yes
    volumes:
      - redis_data:/data
    ports:
      - "6379:6379"
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 5
    networks:
      - news-network

  # FastAPI Application
  api:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: news-api
    environment:
      - DATABASE_URL=postgresql://postgres:postgres@postgres:5432/news_platform
      - REDIS_URL=redis://redis:6379
      - NEWSAPI_KEY=${NEWSAPI_KEY}
      - GUARDIAN_API_KEY=${GUARDIAN_API_KEY}
      - REDDIT_CLIENT_ID=${REDDIT_CLIENT_ID}
      - REDDIT_CLIENT_SECRET=${REDDIT_CLIENT_SECRET}
      - REDDIT_REDIRECT_URI=${REDDIT_REDIRECT_URI}
    ports:
      - "8000:8000"
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - news-network
    volumes:
      - ./app:/app/app  # Mount for hot reload during development
    command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload

volumes:
  postgres_data:
  redis_data:

networks:
  news-network:
    driver: bridge
Docker Compose Design Decisions

Health checks: The depends_on with condition: service_healthy ensures PostgreSQL and Redis are fully ready before your API starts. Without this, your API might try to connect before the database is accepting connections, causing startup failures.

Environment variables: API keys come from your .env file via ${NEWSAPI_KEY} syntax. Database and Redis URLs use service names (postgres and redis) as hostnames because Docker Compose creates a network where services reference each other by name.

Volumes: The postgres_data and redis_data volumes persist data across container restarts. Without them, you'd lose all data when stopping Docker Compose.

Hot reload: The ./app:/app/app volume mount and --reload flag enable automatic reloading during development. Code changes take effect immediately without rebuilding.

Starting Your Local Environment

Terminal - Docker Compose Commands
# Start all services
docker compose up

# Or in detached mode (background):
docker compose up -d

# View logs for specific service:
docker compose logs -f api

# Run database migrations:
docker compose exec api alembic upgrade head

# Stop everything:
docker compose down

# Stop and remove volumes (fresh start):
docker compose down -v

Testing Locally with Docker Compose

Before deploying to AWS, verify your entire stack works locally with Docker Compose. This catches configuration issues, networking problems, and integration bugs in an environment you control. Chapter 27 emphasized local testing first for good reason—debugging in AWS is expensive and slow.

Initial Setup and Database Migrations

Start your services and run database migrations to create your schema:

Terminal - Local Testing Workflow
# 1. Start all services (detached mode)
docker compose up -d

# 2. Verify all containers are healthy
docker compose ps
# Expected output: api (healthy), postgres (healthy), redis (healthy)

# 3. Run database migrations
docker compose exec api alembic upgrade head

# 4. Check application logs
docker compose logs api --follow

# 5. Test the health endpoint
curl http://localhost:8000/health
# Expected: {"status": "healthy", "database": "connected", "redis": "connected"}

# 6. Test article fetching
curl "http://localhost:8000/api/articles?query=python&source=newsapi"
# Should return JSON with articles from NewsAPI

Comprehensive Local Testing Checklist

Test each component systematically before AWS deployment:

  • Database connectivity: Run docker compose exec postgres psql -U postgres -d news_platform -c "\dt" to verify tables exist
  • Redis caching: Make the same API request twice. Second request should be faster (<10ms vs ~800ms). Check logs for cache hits.
  • External API integration: Test each source individually: /api/articles?source=newsapi, source=guardian, source=reddit
  • OAuth flow: Visit http://localhost:8000/auth/reddit/login and complete Reddit authorization. Verify tokens are stored.
  • Error handling: Test with invalid API keys (temporarily change .env values). API should return proper error messages, not crash.
  • Concurrent requests: Use ab (Apache Bench) to send 100 concurrent requests: ab -n 100 -c 10 http://localhost:8000/health

Debugging Common Local Issues

If tests fail, diagnose systematically:

Debugging Commands
# Check container logs for errors
docker compose logs api | grep -i error
docker compose logs postgres | tail -20
docker compose logs redis | tail -20

# Execute commands inside containers
docker compose exec api python -c "from app.database import engine; print(engine)"
docker compose exec postgres psql -U postgres -d news_platform -c "SELECT COUNT(*) FROM articles;"
docker compose exec redis redis-cli PING

# Test network connectivity between containers
docker compose exec api ping postgres
docker compose exec api curl redis:6379

# Restart individual services
docker compose restart api
docker compose restart postgres

# Rebuild API container (if code changes aren't reflected)
docker compose up -d --build api

Only proceed to AWS deployment after all local tests pass consistently. Deploying a broken system to production wastes time and money.

Optimizing for Production: Multi-Stage Builds

Multi-stage Docker builds dramatically reduce image size by separating build dependencies from runtime dependencies. Your final image contains only what's needed to run the application, not the tools needed to build it.

This optimization reduces image size from 500-700MB (single-stage) to 200-300MB (multi-stage). Smaller images mean faster deployments, lower bandwidth costs, and reduced attack surface.

Complete Implementation: Multi-Stage Dockerfile (55 lines - click to expand)
Dockerfile - Multi-Stage Production Build
# ==========================================
# Stage 1: Builder - Install dependencies
# ==========================================
FROM python:3.11-slim as builder

WORKDIR /app

# Install system dependencies for Python packages
RUN apt-get update && apt-get install -y \
    gcc \
    postgresql-client \
    libpq-dev \
    && rm -rf /var/lib/apt/lists/*

# Copy requirements and install
COPY requirements.txt .
RUN pip install --no-cache-dir --user -r requirements.txt

# ==========================================
# Stage 2: Runtime - Minimal final image
# ==========================================
FROM python:3.11-slim

WORKDIR /app

# Install runtime dependencies only
RUN apt-get update && apt-get install -y \
    libpq5 \
    && rm -rf /var/lib/apt/lists/*

# Copy Python packages from builder
COPY --from=builder /root/.local /root/.local

# Copy application code
COPY ./app /app/app
COPY ./alembic /app/alembic
COPY ./alembic.ini /app/alembic.ini

# Make sure scripts in .local are usable
ENV PATH=/root/.local/bin:$PATH

# Create non-root user
RUN useradd -m -u 1000 appuser && \
    chown -R appuser:appuser /app
USER appuser

# Expose port
EXPOSE 8000

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health', timeout=2)"

# Start application
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
Multi-Stage Build Optimizations

Stage separation: Stage 1 (builder) installs compilation tools (gcc, libpq-dev). Stage 2 (runtime) only installs runtime libraries (libpq5). Build tools aren't copied to the final image, saving 200-300MB.

Layer caching: Requirements are copied before application code. When code changes, Docker reuses cached layers for dependencies, making rebuilds much faster.

Security: Non-root user (appuser) runs the application. If an attacker compromises your app, they have limited system access.

Health checks: Docker's HEALTHCHECK monitors your application. Orchestrators (Docker Compose, ECS) can restart unhealthy containers automatically.

Building and Testing the Image

Terminal - Build and Test Docker Image
# Build the image
docker build -t news-api:latest .

# Check image size (should be ~200-300MB)
docker images news-api:latest

# Test the image locally
docker run -p 8000:8000 \
  -e DATABASE_URL=postgresql://postgres:postgres@localhost:5432/news_platform \
  -e REDIS_URL=redis://localhost:6379 \
  --env-file .env \
  news-api:latest

AWS Deployment Walkthrough

Deploying to AWS involves several steps: pushing your Docker image to ECR (Elastic Container Registry), creating an ECS task definition, configuring RDS and ElastiCache, setting up load balancing, and creating the ECS service. Each step is well-documented in Chapter 28, but here's the complete implementation for your project.

Push Image to Amazon ECR

Terminal - Push to ECR
# Authenticate Docker with ECR
aws ecr get-login-password --region us-east-1 | \
    docker login --username AWS --password-stdin \
    <your-account-id>.dkr.ecr.us-east-1.amazonaws.com

# Create repository if it doesn't exist
aws ecr create-repository --repository-name news-platform-api --region us-east-1

# Get repository URI
ECR_URI=$(aws ecr describe-repositories \
    --repository-names news-platform-api \
    --query 'repositories[0].repositoryUri' \
    --output text)

# Tag and push
docker tag news-api:latest $ECR_URI:latest
docker push $ECR_URI:latest

ECS Task Definition

The task definition specifies how your container runs: CPU, memory, environment variables, logging configuration, and health checks.

Complete Implementation: ECS Task Definition (45 lines - click to expand)
task-definition.json - ECS Configuration
{
  "family": "news-platform-task",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "256",
  "memory": "512",
  "executionRoleArn": "arn:aws:iam::<account-id>:role/ecsTaskExecutionRole",
  "containerDefinitions": [
    {
      "name": "news-api",
      "image": "<account-id>.dkr.ecr.us-east-1.amazonaws.com/news-platform-api:latest",
      "portMappings": [
        {
          "containerPort": 8000,
          "protocol": "tcp"
        }
      ],
      "environment": [
        {
          "name": "DATABASE_URL",
          "value": "postgresql://newsadmin:password@<rds-endpoint>:5432/news_platform"
        },
        {
          "name": "REDIS_URL",
          "value": "redis://<elasticache-endpoint>:6379"
        }
      ],
      "secrets": [
        {
          "name": "NEWSAPI_KEY",
          "valueFrom": "arn:aws:secretsmanager:us-east-1:<account-id>:secret:newsapi-key"
        },
        {
          "name": "GUARDIAN_API_KEY",
          "valueFrom": "arn:aws:secretsmanager:us-east-1:<account-id>:secret:guardian-key"
        }
      ],
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/news-platform",
          "awslogs-region": "us-east-1",
          "awslogs-stream-prefix": "api"
        }
      }
    }
  ]
}
Task Definition Best Practices

Secrets Manager: API keys stored in AWS Secrets Manager, not environment variables. This provides encryption at rest, automatic rotation, and audit logging.

Resource limits: 256 CPU units and 512MB memory is sufficient for light-to-moderate traffic. Monitor CloudWatch and adjust if you see throttling.

Logging: CloudWatch Logs captures all stdout/stderr. Use structured logging in your application for easier searching and analysis.

CI/CD Pipeline with GitHub Actions

GitHub Actions automates your deployment pipeline. Every push to main triggers: run tests, build Docker image, push to ECR, update ECS task definition, and deploy to production. The entire process takes 5-8 minutes.

Complete Implementation: GitHub Actions Deploy Workflow (120 lines - click to expand)
.github/workflows/deploy.yml - Complete CI/CD Pipeline
name: Deploy to AWS ECS

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

env:
  AWS_REGION: us-east-1
  ECR_REPOSITORY: news-platform-api
  ECS_SERVICE: news-platform-service
  ECS_CLUSTER: news-platform-cluster
  ECS_TASK_DEFINITION: news-platform-task
  CONTAINER_NAME: news-api

jobs:
  test:
    name: Run Tests
    runs-on: ubuntu-latest

    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: news_platform_test
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432

      redis:
        image: redis:7-alpine
        options: >-
          --health-cmd "redis-cli ping"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 6379:6379

    steps:
      - uses: actions/checkout@v3

      - name: Set up Python 3.11
        uses: actions/setup-python@v4
        with:
          python-version: '3.11'

      - name: Cache dependencies
        uses: actions/cache@v3
        with:
          path: ~/.cache/pip
          key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}

      - name: Install dependencies
        run: |
          pip install -r requirements.txt
          pip install pytest pytest-cov pytest-asyncio

      - name: Run tests
        env:
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/news_platform_test
          REDIS_URL: redis://localhost:6379
        run: |
          pytest --cov=app --cov-report=term-missing --cov-fail-under=70

  deploy:
    name: Deploy to AWS
    needs: test
    runs-on: ubuntu-latest
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'

    steps:
      - uses: actions/checkout@v3

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v2
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ env.AWS_REGION }}

      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v1

      - name: Build, tag, and push image
        id: build-image
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          IMAGE_TAG: ${{ github.sha }}
        run: |
          docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
          docker tag $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG $ECR_REGISTRY/$ECR_REPOSITORY:latest
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest
          echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT

      - name: Update ECS task definition
        id: task-def
        uses: aws-actions/amazon-ecs-render-task-definition@v1
        with:
          task-definition: task-definition.json
          container-name: ${{ env.CONTAINER_NAME }}
          image: ${{ steps.build-image.outputs.image }}

      - name: Deploy to ECS
        uses: aws-actions/amazon-ecs-deploy-task-definition@v1
        with:
          task-definition: ${{ steps.task-def.outputs.task-definition }}
          service: ${{ env.ECS_SERVICE }}
          cluster: ${{ env.ECS_CLUSTER }}
          wait-for-service-stability: true
CI/CD Pipeline Design

Test before deploy: The deploy job only runs if test succeeds. Failed tests block deployment automatically.

Branch protection: if: github.ref == 'refs/heads/main' ensures only main branch pushes trigger deployment. Pull requests run tests but don't deploy.

Image tagging: Images are tagged with both commit SHA (immutable, traceable) and latest (convenience). You can always rollback to a specific commit.

Deployment verification: wait-for-service-stability: true monitors ECS deployment. If new tasks fail health checks, the job fails and you're notified.

CloudWatch Monitoring Dashboard

CloudWatch dashboards visualize the Golden Signals: latency (response times at different percentiles), traffic (requests per minute), errors (4xx and 5xx counts), and saturation (CPU and memory utilization). A well-designed dashboard shows system health at a glance.

This dashboard configuration tracks all four Golden Signals specific to your News Intelligence Platform. You'll monitor both the Application Load Balancer (for request metrics) and ECS service (for container metrics).

Complete Implementation: CloudWatch Dashboard (70 lines - click to expand)
dashboard-config.json - Golden Signals Dashboard
{
  "widgets": [
    {
      "type": "metric",
      "properties": {
        "metrics": [
          ["AWS/ApplicationELB", "TargetResponseTime", {"stat": "Average"}],
          ["...", {"stat": "p99"}]
        ],
        "period": 60,
        "stat": "Average",
        "region": "us-east-1",
        "title": "Latency (Response Time)",
        "yAxis": {
          "left": {
            "min": 0,
            "label": "Seconds"
          }
        }
      }
    },
    {
      "type": "metric",
      "properties": {
        "metrics": [
          ["AWS/ApplicationELB", "RequestCount", {"stat": "Sum"}]
        ],
        "period": 60,
        "stat": "Sum",
        "region": "us-east-1",
        "title": "Traffic (Requests Per Minute)"
      }
    },
    {
      "type": "metric",
      "properties": {
        "metrics": [
          ["AWS/ApplicationELB", "HTTPCode_Target_5XX_Count", {"stat": "Sum"}],
          ["AWS/ApplicationELB", "HTTPCode_Target_4XX_Count", {"stat": "Sum"}]
        ],
        "period": 60,
        "stat": "Sum",
        "region": "us-east-1",
        "title": "Errors (By Status Code)"
      }
    },
    {
      "type": "metric",
      "properties": {
        "metrics": [
          ["AWS/ECS", "CPUUtilization", {"stat": "Average", "dimensions": {"ServiceName": "news-platform-service", "ClusterName": "news-platform-cluster"}}],
          ["AWS/ECS", "MemoryUtilization", {"stat": "Average", "dimensions": {"ServiceName": "news-platform-service", "ClusterName": "news-platform-cluster"}}]
        ],
        "period": 60,
        "stat": "Average",
        "region": "us-east-1",
        "title": "Saturation (Resource Utilization)"
      }
    }
  ]
}
Dashboard Design Decisions

Golden Signals: These four metrics (latency, traffic, errors, saturation) are the minimum viable monitoring. If all four look healthy, your system is probably fine. If any show problems, you know where to investigate.

P99 latency: Average latency hides outliers. P99 shows worst-case experience for 1% of requests. If P99 is high but average is low, some users are having a bad experience.

Error breakdown: Tracking 4xx and 5xx separately helps diagnosis. High 4xx might mean client problems (bad requests). High 5xx means your code has bugs.

Creating the Dashboard

Terminal - Create CloudWatch Dashboard
# Create dashboard from config file
aws cloudwatch put-dashboard \
    --dashboard-name NewsPlatformProduction \
    --dashboard-body file://dashboard-config.json

# View dashboard URL
echo "https://console.aws.amazon.com/cloudwatch/home?region=us-east-1#dashboards:name=NewsPlatformProduction"

After completing Phase 2, your News Intelligence Platform runs on AWS with professional infrastructure: containers auto-scaling based on CPU (2-10 tasks), database with automated backups (RDS PostgreSQL), caching layer for performance (ElastiCache Redis), load balancer distributing traffic (Application Load Balancer), CI/CD deploying on every push (GitHub Actions), and comprehensive monitoring tracking system health (CloudWatch).

Troubleshooting Common Deployment Issues

Even with careful configuration, AWS deployments encounter predictable issues. This section documents the most common problems you'll face and their solutions. Bookmark this page—you'll reference it multiple times during deployment.

Issue 1: ECS Tasks Keep Restarting

Symptoms: ECS service shows tasks starting and immediately stopping. Health checks never pass. CloudWatch logs show startup errors or database connection failures.

Diagnosis:

Debugging ECS Task Failures
# 1. Check CloudWatch logs for startup errors
aws logs tail /ecs/news-api --follow

# 2. Verify DATABASE_URL is correctly formatted
# Should be: postgresql://username:password@rds-endpoint:5432/dbname

# 3. Test database connectivity from task
# Use ECS Exec to shell into running task
aws ecs execute-command --cluster news-cluster \
  --task TASK_ID --container api \
  --command "/bin/sh" --interactive

# Inside container, test database:
python -c "import psycopg2; psycopg2.connect('postgresql://...')"

Common Fixes:

  • Verify RDS security group allows inbound traffic on port 5432 from ECS tasks
  • Confirm DATABASE_URL environment variable is set correctly in task definition
  • Check RDS instance is in "available" state, not "backing-up" or "modifying"
  • Ensure task execution role has permissions to pull secrets from Secrets Manager
  • Run database migrations: alembic upgrade head may not have completed

Issue 2: ALB Health Checks Failing

Symptoms: Application Load Balancer shows all targets as "unhealthy." Tasks are running but not receiving traffic. /health endpoint returns errors or timeouts.

Common Fixes:

  • Verify health check path is /health (not /) in target group settings
  • Check ECS task security group allows inbound HTTP (port 8000) from ALB security group
  • Confirm tasks have public IP addresses OR routes to ALB (check subnet configuration)
  • Test health endpoint directly: curl http://TASK_PUBLIC_IP:8000/health
  • Review health check thresholds: 2 consecutive failures with 30s interval is too aggressive. Try 3 failures, 10s interval.
  • Check application logs for errors during health check handling

Issue 3: External APIs Timing Out

Symptoms: Requests to NewsAPI, Guardian, or Reddit timeout. Works locally but fails in ECS. CloudWatch logs show httpx.TimeoutException or ConnectionError.

Common Fixes:

  • Verify ECS task security group allows outbound HTTPS traffic (port 443) to 0.0.0.0/0
  • If using private subnets, confirm NAT Gateway is configured and routes 0.0.0.0/0 → NAT Gateway
  • Test external connectivity from inside container: curl https://newsapi.org
  • Check VPC route tables: private subnets need route to NAT Gateway, public subnets to Internet Gateway
  • Increase httpx timeout from 30s to 60s for external API calls

Issue 4: Redis Connection Refused

Symptoms: Cache operations fail. Logs show redis.exceptions.ConnectionError: Error 111 connecting to redis:6379. Connection refused.

Common Fixes:

  • Verify ElastiCache endpoint is correct in REDIS_URL environment variable
  • Check ElastiCache security group allows inbound traffic on port 6379 from ECS tasks
  • Confirm ElastiCache cluster is in same VPC as ECS tasks
  • ElastiCache doesn't have public endpoints—tasks must be in same VPC
  • Test connectivity: telnet REDIS_ENDPOINT 6379 from inside ECS task

Issue 5: Secrets Manager Access Denied

Symptoms: Tasks fail to start with error: Error retrieving secret: User is not authorized to perform: secretsmanager:GetSecretValue.

Common Fixes:

  • Verify task execution role (not task role) has secretsmanager:GetSecretValue permission
  • Check secret ARN in task definition matches actual secret ARN
  • Confirm secret exists in same region as ECS cluster
  • Review IAM policy attached to task execution role for typos in resource ARN

Issue 6: CI/CD Pipeline Deployment Timeouts

Symptoms: GitHub Actions workflow times out waiting for ECS service to stabilize. Deployment takes >15 minutes or never completes.

Common Fixes:

  • Check ECS deployment circuit breaker settings—may be rolling back due to health check failures
  • Review ECS service events: aws ecs describe-services --cluster news-cluster --services news-api
  • Verify new task definition is valid: incorrect environment variables cause infinite restart loops
  • Increase GitHub Actions timeout from 10 minutes to 20 minutes for initial deployments
  • Check if ECS cluster has capacity: insufficient CPU/memory prevents new tasks from starting

General Debugging Strategy

When facing deployment issues, follow this systematic approach:

  1. Check CloudWatch logs first: 90% of issues reveal themselves in application logs
  2. Verify security groups: Most AWS connectivity issues are security group misconfigurations
  3. Test one component at a time: Isolate database, Redis, external APIs, health checks independently
  4. Compare to working local environment: What's different? Environment variables? Network access?
  5. Use AWS Console, not just CLI: Visual diagrams often reveal misconfigured networking
  6. Check AWS Service Health Dashboard: Occasionally issues are AWS service problems, not your code

Professional DevOps engineers spend 50% of their time debugging these exact issues. Troubleshooting is a core competency, not a failure. Document what you learn—you'll reference these solutions repeatedly.

5. Phase 3: Choose Your Extension

You've built a complete production system: multi-source news aggregation, OAuth authentication, PostgreSQL storage, Redis caching, AWS deployment, and CI/CD automation. This demonstrates breadth. You can build full-stack systems. Extensions demonstrate depth. You can specialize in specific technical domains.

Professional engineering roles require both. Breadth shows you understand systems holistically. Depth shows you can go beyond surface-level implementation when challenges demand it. Extensions are your opportunity to showcase depth in an area that interests you.

You'll implement one or two extensions from the options below. Choose based on career interests, learning goals, portfolio differentiation, and time availability.

Extension Options Overview

Why Extensions Matter

Extensions differentiate your project. Everyone builds the same core platform (ensuring you demonstrate all required skills). Extensions show specialization. When employers review your project, they see consistent fundamentals plus your chosen depth area.

Choosing Your Extension

Consider these factors when choosing:

Career interests: Want to work in real-time systems? Choose WebSocket alerts. Interested in ML/data science? Choose sentiment analysis. Focused on infrastructure? Choose advanced caching or email systems.

Learning goals: Pick extensions that push you outside your comfort zone. If you're strong in backend but weak in frontend, real-time alerts with WebSocket gives you frontend challenges. If you're comfortable with CRUD but haven't done ML, sentiment analysis is perfect.

Portfolio differentiation: What makes your project unique? "I built news aggregation with real-time sentiment analysis and personalized recommendations" is more memorable than "I built news aggregation."

Time availability: Extensions range from 5-10 hours. Pick complexity that fits your timeline. Sentiment analysis is the simplest (5-6 hours). Multi-language support is the most complex (10-12 hours).

Extension Feature Matrix

Extension Complexity Time Est. Technical Skills Career Path
Sentiment Analysis ⭐⭐☆☆☆ 5-6 hrs ML integration, data analysis Data/ML engineering
Real-Time Alerts ⭐⭐⭐☆☆ 8-10 hrs WebSockets, async, pub/sub Real-time systems, frontend
Smart Recommendations ⭐⭐⭐☆☆ 7-9 hrs Algorithms, user modeling Recommendation systems, product
Email Digests ⭐⭐⭐☆☆ 6-8 hrs Background jobs, email services Platform/infrastructure
Advanced Caching ⭐⭐⭐⭐☆ 8-10 hrs Performance optimization, metrics Performance/SRE
Multi-Language ⭐⭐⭐⭐☆ 10-12 hrs Internationalization, translation APIs Global products

What Each Extension Adds

Extension A: Sentiment Analysis - Add ML-powered sentiment scoring to every article. Users see whether coverage is positive, negative, or neutral. Aggregate sentiment trends over time. "Trump coverage sentiment dropped 15 points this week."

Extension B: Real-Time Alerts - WebSocket connections push notifications when articles matching user keywords are published. Subscribe to "climate change" and get instant notifications when new articles appear. Frontend challenge plus backend pub/sub pattern.

Extension C: Smart Recommendations - Analyze user reading behavior and recommend articles they'll find interesting. Collaborative filtering: "Users who read this also read..." Content-based filtering: "Similar to articles you've saved." Increases engagement.

Extension D: Email Digests - Scheduled background jobs generate personalized email summaries. Users configure preferences: daily digest at 8am, top 5 articles about "python programming", unsubscribe anytime. Production email infrastructure.

Extension E: Advanced Caching - Multi-tier caching strategy with L1 (in-memory), L2 (Redis), cache warming (pre-populate popular searches), and intelligent invalidation. Includes cache hit rate metrics and performance analysis.

Extension F: Multi-Language Support - Integrate international news sources (BBC Mundo, Le Monde), detect article language, provide translation via Google Translate API, enable language-based filtering. Expands your platform globally.

Extension A - Sentiment Analysis (Complete Walkthrough)

I'll provide a complete implementation of this extension as a reference. Other extensions will be specified clearly but you'll implement them following this pattern.

What You're Building

Add sentiment analysis to every article using TextBlob (simple NLP library). Store sentiment scores in the database. Provide endpoints to query sentiment trends. Enable filtering articles by sentiment ("show me only positive climate news").

Why TextBlob: It's simple, doesn't require ML expertise, works offline (no API keys), and provides good-enough accuracy for demonstration purposes. Production systems might use more sophisticated models (BERT, RoBERTa), but TextBlob proves the concept.

Step 1: Install Dependencies

Add TextBlob to requirements.txt and install the language models:

requirements.txt - Add this line
textblob==0.17.1
Terminal - Install TextBlob
pip install textblob
python -m textblob.download_corpora

Step 2: Create Sentiment Database Model

Add a new table to store sentiment scores. Each article gets one sentiment record with polarity (positive/negative scale), subjectivity (objective/subjective scale), and categorical label.

app/schemas/sentiment.py
"""
Sentiment analysis database model.
"""
from sqlalchemy import Column, Integer, Float, String, ForeignKey, DateTime, Index
from sqlalchemy.sql import func
from app.database import Base


class ArticleSentiment(Base):
    """Sentiment scores for articles."""
    __tablename__ = "article_sentiments"

    id = Column(Integer, primary_key=True, index=True)
    article_id = Column(Integer, ForeignKey("articles.id", ondelete="CASCADE"), 
                       nullable=False, unique=True)

    # Sentiment metrics
    polarity = Column(Float, nullable=False)  # -1.0 (negative) to 1.0 (positive)
    subjectivity = Column(Float, nullable=False)  # 0.0 (objective) to 1.0 (subjective)

    # Categorical classification
    sentiment_label = Column(String(20), nullable=False)  # 'positive', 'negative', 'neutral'
    confidence = Column(Float)  # How confident we are (based on polarity magnitude)

    # Analysis metadata
    analyzed_at = Column(DateTime(timezone=True), server_default=func.now())
    text_length = Column(Integer)  # Characters analyzed

    __table_args__ = (
        Index('idx_sentiment_label', 'sentiment_label'),
        Index('idx_polarity', 'polarity'),
        Index('idx_article_id', 'article_id'),
    )

Generate and apply the database migration:

Terminal - Create Migration
alembic revision --autogenerate -m "add article sentiment analysis"
alembic upgrade head

Step 3: Create Sentiment Analysis Service

Build a service class that analyzes text and returns sentiment metrics. This class uses TextBlob to calculate polarity and subjectivity, then classifies the result as positive/negative/neutral based on thresholds.

Understanding Sentiment Metrics

Polarity: Ranges from -1.0 (very negative) to +1.0 (very positive). "This is terrible" scores around -0.7. "This is amazing" scores around +0.6. Neutral factual statements score near 0.0.

Subjectivity: Ranges from 0.0 (objective fact) to 1.0 (subjective opinion). "The temperature is 72 degrees" scores 0.0. "I love sunny weather" scores 0.8. Sentiment analysis works better on subjective text.

Classification thresholds: Polarity > 0.1 = positive, < -0.1 = negative, -0.1 to 0.1 = neutral. These thresholds prevent barely-positive text from being classified as positive.

Complete SentimentAnalyzer Service Implementation Click to expand 108-line implementation
app/services/sentiment_analyzer.py
"""
Sentiment analysis service using TextBlob.
"""
from typing import Dict, Optional
from textblob import TextBlob
from sqlalchemy.orm import Session
from app.schemas.sentiment import ArticleSentiment


class SentimentAnalyzer:
    """Analyzes text sentiment and stores results."""
    
    # Classification thresholds
    POSITIVE_THRESHOLD = 0.1
    NEGATIVE_THRESHOLD = -0.1
    
    def analyze_text(self, text: str) -> Dict[str, float]:
        """
        Analyze sentiment of given text.
        
        Args:
            text: Text to analyze
            
        Returns:
            Dictionary with polarity, subjectivity, sentiment_label, confidence
        """
        if not text or not text.strip():
            return {
                "polarity": 0.0,
                "subjectivity": 0.0,
                "sentiment_label": "neutral",
                "confidence": 0.0,
                "text_length": 0
            }
        
        # Analyze with TextBlob
        blob = TextBlob(text)
        polarity = blob.sentiment.polarity
        subjectivity = blob.sentiment.subjectivity
        
        # Classify sentiment
        if polarity > self.POSITIVE_THRESHOLD:
            sentiment_label = "positive"
        elif polarity < self.NEGATIVE_THRESHOLD:
            sentiment_label = "negative"
        else:
            sentiment_label = "neutral"
        
        # Calculate confidence (how far from neutral)
        confidence = abs(polarity)
        
        return {
            "polarity": polarity,
            "subjectivity": subjectivity,
            "sentiment_label": sentiment_label,
            "confidence": confidence,
            "text_length": len(text)
        }
    
    def analyze_article(
        self, 
        article_id: int, 
        title: str, 
        description: Optional[str],
        content: Optional[str],
        db: Session
    ) -> ArticleSentiment:
        """
        Analyze article sentiment and store in database.
        
        Args:
            article_id: ID of article to analyze
            title: Article title
            description: Article description/summary
            content: Full article content (if available)
            db: Database session
            
        Returns:
            ArticleSentiment database object
        """
        # Combine all available text
        text_parts = [title]
        if description:
            text_parts.append(description)
        if content:
            text_parts.append(content)
        
        combined_text = " ".join(text_parts)
        
        # Analyze sentiment
        sentiment_data = self.analyze_text(combined_text)
        
        # Check if sentiment already exists
        existing = db.query(ArticleSentiment).filter(
            ArticleSentiment.article_id == article_id
        ).first()
        
        if existing:
            # Update existing record
            existing.polarity = sentiment_data["polarity"]
            existing.subjectivity = sentiment_data["subjectivity"]
            existing.sentiment_label = sentiment_data["sentiment_label"]
            existing.confidence = sentiment_data["confidence"]
            existing.text_length = sentiment_data["text_length"]
            db.commit()
            db.refresh(existing)
            return existing
        
        # Create new sentiment record
        sentiment = ArticleSentiment(
            article_id=article_id,
            polarity=sentiment_data["polarity"],
            subjectivity=sentiment_data["subjectivity"],
            sentiment_label=sentiment_data["sentiment_label"],
            confidence=sentiment_data["confidence"],
            text_length=sentiment_data["text_length"]
        )
        
        db.add(sentiment)
        db.commit()
        db.refresh(sentiment)
        
        return sentiment

Steps 4-8: Integration and Testing

The remaining implementation steps follow this pattern:

  • Step 4: Integrate sentiment analysis into article storage. When articles are saved, automatically analyze and store sentiment.
  • Step 5: Add sentiment endpoints: GET /articles/{id}/sentiment (individual scores), GET /articles/sentiment/trends (time series analysis), GET /articles/sentiment/summary (overall distribution).
  • Step 6: Enhance search endpoint with sentiment filter: ?sentiment=positive parameter.
  • Step 7: Create Pydantic response models for API validation and documentation.
  • Step 8: Write comprehensive tests: positive/negative/neutral detection, empty text handling, endpoint integration tests.

Steps 9-10: Documentation and Demo

Update your README with sentiment analysis documentation explaining features, API endpoints, technical implementation, use cases. Record a demo video showing: search with mixed sentiment, filter to positive-only results, sentiment trend visualization, detailed sentiment metrics for specific articles.

This complete walkthrough demonstrates the level of implementation expected for any extension. The pattern: add dependencies, create database models, build service logic, integrate with existing endpoints, test comprehensively, document thoroughly.

Additional Extension Options

The remaining extensions follow the same implementation pattern demonstrated in the sentiment analysis walkthrough. Each includes clear specifications, required components, and expected deliverables.

Extension B: Real-Time Alerts (8-10 hours)

WebSocket connections that push live notifications when articles matching user keywords are published. Architecture: Background worker polls external APIs every 5 minutes, new articles checked against user subscriptions, matching articles published to Redis pub/sub channel, WebSocket connections receive notifications, frontend displays real-time toasts. Technical additions: WebSocket endpoint /ws/alerts, user subscription model storing keywords, Celery background worker, Redis pub/sub integration, JWT authentication for WebSocket connections, simple HTML/JavaScript frontend for demo.

Extension C: Smart Recommendations (7-9 hours)

Analyze user reading behavior and recommend articles they'll find interesting. Algorithms: Collaborative filtering (users who read X also read Y), content-based filtering (similar to articles you've saved), popularity-weighted ranking. Implementation: User interaction tracking (clicks, saves, reading time), similarity computation using TF-IDF or embeddings, recommendation endpoint returning personalized article list, A/B testing framework to measure recommendation quality.

Extension D: Email Digests (6-8 hours)

Scheduled background jobs generate personalized email summaries. Components: Celery Beat for scheduling, SendGrid API for email delivery, HTML email templates, user preference model (frequency, topics, time), unsubscribe mechanism, email tracking (opens, clicks). Deliverables: Daily/weekly digest options, topic-based filtering, responsive HTML templates, preference management UI.

Extension E: Advanced Caching (8-10 hours)

Multi-tier caching strategy with comprehensive metrics. Implementation: L1 cache (in-memory LRU with 1000-item limit), L2 cache (Redis with 5-minute TTL), cache warming (pre-populate top 100 searches), intelligent invalidation (clear on new articles), cache hit rate tracking, performance comparison metrics, cache inspection endpoints. Metrics: Hit rate by cache tier, average response time with/without cache, memory usage, eviction rates.

Extension F: Multi-Language Support (10-12 hours)

Integrate international news sources with automatic translation. Sources: BBC Mundo (Spanish), Le Monde (French), Deutsche Welle (German), Al Jazeera (Arabic). Features: Language detection using langdetect library, translation via Google Translate API, language-based filtering, original language preservation, translation caching. Complexity factors: Multiple new API integrations, character encoding handling, RTL language support, translation cost optimization.

Choosing Multiple Extensions

If you implement two extensions, choose complementary ones. Good pairs: Sentiment Analysis + Real-Time Alerts (push sentiment scores), Smart Recommendations + Advanced Caching (cache recommendation results), Email Digests + Sentiment Analysis (filter by sentiment in emails). Avoid redundant combinations.

6. Documentation & Presentation

Technical competency gets you interviews. Professional presentation gets you offers. Your README is the first thing recruiters see. Your demo video demonstrates communication skills. Your architecture diagrams show systems thinking. Your reflection reveals learning ability.

This section covers creating portfolio-quality documentation. The difference between "built a project" and "built a professional system" often comes down to presentation, not code.

Writing a Professional README

Your README is the first thing recruiters and employers see. It needs to be comprehensive but scannable. Hiring managers spend 30-60 seconds on initial review. Make those seconds count.

Required README Sections

1

Project Title and Tagline

Start with a clear, descriptive name and one-sentence summary. Example: "News Intelligence Platform: Multi-source news aggregation API with intelligent caching, sentiment analysis, and production AWS deployment."

2

Badges and Links

Add build status, coverage percentage, and quick links. Include: Live Demo URL, API Documentation (your /docs endpoint), and Video Demo link.

3

Overview and Key Features

Two-paragraph overview explaining what the system does and why it matters. Follow with bulleted key features highlighting production aspects: multi-source aggregation, OAuth authentication, Redis caching with performance metrics, AWS deployment, CI/CD automation, monitoring.

4

Architecture and Tech Stack

Include architecture diagram (more on this in 6.2). List tech stack organized by category: Backend (FastAPI, Python, SQLAlchemy), Databases (PostgreSQL, Redis), Infrastructure (AWS ECS, RDS, ElastiCache, ALB), CI/CD (GitHub Actions), Monitoring (CloudWatch).

5

Quick Start Guide

Step-by-step local setup: clone repo, create virtual environment, configure .env file, start Docker Compose, run migrations, access API. Make it copy-paste ready. Include example API requests that work immediately.

6

API Documentation

Document your main endpoints with example requests and responses. Show authentication flow, search parameters, filtering options. Link to full docs at /docs endpoint.

7

Deployment Section

Explain AWS infrastructure: which services you used and why. Include cost analysis (monthly estimates for Free Tier and beyond). Show deployment commands. Describe CI/CD pipeline stages.

8

Performance Benchmarks

Include measurable results. Example: "Average response time: 687ms without caching → 4ms with Redis (162x improvement). Throughput: 12 req/sec → 419 req/sec (35x increase)." Numbers demonstrate impact.

9

Project Structure

Show directory tree explaining organization. Helps reviewers navigate your codebase quickly.

10

Monitoring, Security, Known Limitations

Document CloudWatch dashboard metrics, alarms, security measures (OAuth, JWT, secrets management). Be honest about limitations (API rate limits, free tier constraints, sentiment accuracy). Shows maturity.

README Anti-Patterns to Avoid

Wall of text: Break up long paragraphs. Use headers, bullets, code blocks for scannability.

Vague descriptions: "Fast API" means nothing. "4ms average response time with Redis caching" is specific and meaningful.

No visuals: At minimum, include architecture diagram and one screenshot. Visual elements increase engagement.

Broken links: Test every link before submitting. Dead links signal low attention to detail.

Creating Architecture Diagrams

Architecture diagrams communicate system design visually. They help interviewers understand your work quickly and demonstrate systems thinking.

Recommended Tools

  • Excalidraw (free, web-based): excalidraw.com - Hand-drawn style, quick to create, looks professional
  • Draw.io (free, powerful): app.diagrams.net - More formal, extensive shape libraries, AWS icon sets
  • Lucidchart (paid, professional): Advanced features, collaboration, templates

Five Diagrams to Create

1. High-Level Architecture: System overview showing all major components (FastAPI, PostgreSQL, Redis, external APIs, load balancer) and how they connect. This is your "30,000-foot view" diagram.

2. Request Flow Diagram: Step-by-step data flow through the system. Start with user request, show each processing step (authentication, cache check, external API call, database storage, response), include timing estimates for each step.

3. Database Schema (ER Diagram): Tables, columns, relationships, foreign keys, indexes. Use proper ER diagram notation (crow's foot for relationships). Shows database design skills.

4. Deployment Architecture: AWS infrastructure layout showing VPC, subnets, security groups, ECS tasks, RDS instance, ElastiCache cluster, load balancer, CloudWatch. Label everything clearly.

5. CI/CD Pipeline: GitHub Actions workflow visualization: test stage, build stage, push to ECR, deploy to ECS, health check verification. Show quality gates and failure paths.

Export all diagrams as PNG at high resolution (1920px width minimum) and commit to docs/ directory. Reference them in README with ![Architecture](docs/architecture.png) syntax.

Recording Your Demo Video

A 5-10 minute demo video demonstrates communication skills, technical depth, and ability to explain complex systems. This is interview practice.

Video Structure

Introduction (30 seconds): State your name and project. "Hi, I'm [name], and this is the News Intelligence Platform, a production-grade news aggregation API deployed on AWS."

Architecture Overview (1-2 minutes): Show architecture diagram. Explain technology choices. Highlight production aspects (auto-scaling, monitoring, CI/CD). Keep it high-level.

Live Demonstration (3-4 minutes): Show Swagger UI at /docs. Execute API requests (search articles, filter by sentiment). Show cached vs uncached response times. Demonstrate OAuth flow. Show your extension feature.

Infrastructure Tour (2-3 minutes): AWS Console showing running ECS tasks. CloudWatch Dashboard pointing out metrics. GitHub Actions showing recent deployment. Logs Insights running a query. This proves it's actually deployed.

Code Walkthrough (1-2 minutes): Show one interesting code snippet (caching decorator, sentiment analysis, OAuth implementation). Explain the design decision. Highlight testing approach.

Conclusion (30 seconds): Recap key accomplishments. Technologies mastered. Where to find the code (GitHub link).

Recording Tools and Tips

  • Loom (loom.com): Free, easy to use, web-based recording
  • OBS Studio (obsproject.com): Free, powerful, more control over quality
  • macOS QuickTime: Built-in screen recording (Cmd+Shift+5)

Production tips: Write a script and practice twice before recording. Use 1920x1080 resolution. Zoom in on important details (don't make viewers squint at small text). Speak slowly and clearly. Include captions/subtitles if possible. Upload to YouTube as unlisted. Add the link to your README.

Writing Your Reflection

Create docs/REFLECTION.md with 2-3 pages covering four topics. This document demonstrates learning ability and self-awareness.

1. Biggest Technical Challenge

Describe one significant problem you faced and how you solved it. Be specific. Good example: "Challenge: Redis Connection Pooling in Async Context. Initially, I created a new Redis connection for every request, which caused connection exhaustion under load. Solution: Implemented connection pooling using redis.asyncio.ConnectionPool with max_connections=50. Lesson: Resource pooling is critical in async systems."

2. What You'd Do Differently

If starting over, what would you change? Shows you learned from experience. Example: "I would implement proper secrets management from day one using AWS Secrets Manager instead of environment variables. Refactoring later required updating ECS task definitions and redeploying."

3. Most Valuable Lesson

What's the most important thing you learned? Example: "The importance of monitoring before problems occur. My CloudWatch dashboard caught a slow memory leak that would've caused outages if unnoticed. Production isn't 'set and forget.' It's continuous observation and improvement."

4. Interview Preparation

How would you explain this project in a technical interview? Write your 30-second pitch. List technical deep-dive topics you're prepared to discuss (OAuth challenges, caching strategy, auto-scaling configuration, CI/CD quality gates, cost optimization).

Portfolio Presentation Guide

How you present your project to recruiters determines whether they see a "tutorial follower" or a "production engineer." This guide helps you position your capstone for maximum impact.

README Structure for Recruiters

Your README should enable a recruiter to evaluate your project in 60 seconds while providing enough depth for technical review. Structure it strategically:

  • Live demo link with sample API key: Put this at the top. Recruiters test immediately. Include example curl commands they can copy-paste: curl -H "X-API-Key: demo123" https://api.yourdomain.com/articles?query=python
  • Architecture diagram from Section 2: Visual system overview showing external APIs, your application, database, cache, AWS infrastructure. Recruiters understand systems visually faster than through text.
  • Key technical decisions with rationale: Explain 3-4 significant choices:
    • "Chose FastAPI over Flask for async support and automatic OpenAPI documentation"
    • "Implemented multi-stage Docker builds reducing image size from 650MB to 220MB"
    • "Redis caching with 5-minute TTL improved response times from 800ms to 5ms"
    • "ECS auto-scaling configured to respond within 90 seconds based on CPU metrics"
  • Performance metrics with before/after: Show impact quantitatively:
    • "Cache hit rate: 85% for repeated queries"
    • "API response time: p50 = 12ms, p95 = 45ms, p99 = 120ms"
    • "Test coverage: 78% (measured with pytest --cov)"
    • "CI/CD deployment time: 6 minutes from commit to production"
  • CI/CD pipeline diagram: Show the automated workflow: push to main → tests run → Docker build → ECR push → ECS deployment → health checks → rollback on failure
  • Extension feature highlight: If you implemented sentiment analysis, show example outputs with positive/negative/neutral classifications and explain the ML approach

Interview Talking Points

Prepare specific stories about technical decisions and challenges. Interviewers ask "Tell me about a time you..." questions. Have concrete examples ready:

1

"Describe a technical challenge you faced and how you resolved it"

Example: "When deploying to ECS, my tasks kept restarting after 30 seconds. CloudWatch logs showed database connection timeouts. I discovered the issue was security group configuration—the ECS tasks couldn't reach RDS on port 5432. I created a security group allowing inbound from the ECS security group and verified connectivity using ECS Exec to shell into a running task. This taught me the importance of systematic network debugging in AWS."

2

"How did you ensure your API could handle production load?"

Example: "I implemented three strategies: aggressive Redis caching with 5-minute TTLs reduced external API calls by 85%; database query optimization with proper indexes on published_at and full-text search columns; and ECS auto-scaling policies that spin up additional tasks when CPU exceeds 70%. I load tested with Apache Bench to verify the system could handle 100 concurrent requests without degradation."

3

"Walk me through your deployment process"

Example: "Every commit to main triggers our GitHub Actions pipeline. First, pytest runs with coverage checks—builds fail if coverage drops below 70%. Then we build a multi-stage Docker image optimized from 650MB to 220MB, push to ECR, and update the ECS task definition. ECS performs a rolling deployment with circuit breakers—if health checks fail on new tasks, it automatically rolls back. CloudWatch monitors the deployment with alarms on error rates and response times. Total deployment time is 6-8 minutes."

4

"How did you make architectural trade-offs?"

Example: "For authentication, I implemented three patterns: API keys for NewsAPI (simple, widely supported), no authentication for Guardian (public data), and OAuth 2.0 for Reddit (required for user-specific content). I considered JWT for our own API but chose session-based auth initially because it's simpler to implement and audit. The trade-off is scalability—sessions require sticky sessions or shared storage—but for our current load, this was acceptable. I'd migrate to JWT if we needed to scale beyond 1000 requests/second."

Common Interview Questions About This Project

Prepare answers for these frequently asked questions:

  • "Why FastAPI instead of Flask?" → Async support for concurrent external API calls, automatic OpenAPI documentation, better performance for I/O-bound operations, built-in request validation with Pydantic
  • "How do you handle API rate limits?" → Redis caching reduces external calls by 85%, exponential backoff with retry logic for transient failures, monitoring to track rate limit proximity
  • "What would you do differently if you built this again?" → Implement request queuing with Celery for background article fetching, add API versioning from the start, use JWT instead of sessions for better scalability, implement more granular caching with cache tagging
  • "How do you monitor production?" → CloudWatch dashboards tracking Golden Signals (latency p50/p90/p99, request rate, error rate, CPU/memory), alarms on 5xx errors and high latency, log aggregation for debugging
  • "What's your deployment rollback strategy?" → ECS deployment circuit breakers automatically roll back on health check failures, manual rollback via reverting task definition to previous version, database migrations use Alembic's downgrade functionality

Demonstrating Continuous Learning

Show that you didn't just complete a project—you learned systematically:

  • Document decisions: Include a DECISIONS.md file explaining why you chose specific technologies, patterns, or approaches
  • Show iteration: Your git history should show incremental progress, not one massive commit. Commit messages should explain "why" not just "what"
  • Highlight learning: In your README or project presentation, mention specific concepts you learned: "Implemented OAuth 2.0 for the first time, learning about authorization code grants, token refresh flows, and PKCE"
  • Future improvements section: List 3-4 features you'd add next with brief technical explanations. Shows you're thinking beyond the current implementation.

The goal isn't to memorize these talking points—it's to understand your project deeply enough that you can discuss any aspect confidently. Prepare by reviewing your code, remembering specific debugging sessions, and articulating the reasoning behind technical choices.

7. Evaluation & Submission

Completion Checklist

Use this checklist to verify you've completed all requirements before submission. Each checked item represents demonstrable competency.

Core Requirements (Must Complete)

External APIs

  • Integrated NewsAPI with API key authentication
  • Integrated Guardian API (no auth)
  • Integrated Reddit API with OAuth 2.0
  • Standardized response format across all sources
  • Error handling for API failures

Database

  • PostgreSQL schema with all tables (users, articles, preferences, etc.)
  • Alembic migrations working
  • Full-text search index implemented
  • Proper relationships and constraints

Your API

  • FastAPI with 5+ endpoints
  • Pydantic validation on all inputs
  • Error responses (4xx, 5xx) with clear messages
  • Pagination support
  • API documentation at /docs

Authentication

  • OAuth 2.0 authorization flow
  • Token exchange working
  • JWT token issuance
  • Protected endpoints requiring authentication

Caching

  • Redis integration
  • Cache-aside pattern implementation
  • TTL configuration (5-15 minutes)
  • Demonstrable performance improvement

Testing

  • 70%+ code coverage
  • Unit tests for business logic
  • Integration tests for endpoints
  • API client tests

Containerization

  • Multi-stage Dockerfile
  • Docker Compose for local development
  • All services (API, PostgreSQL, Redis) working together

AWS Deployment

  • ECS Fargate service running
  • RDS PostgreSQL database
  • ElastiCache Redis
  • Application Load Balancer
  • Public URL accessible
  • Health checks passing

CI/CD

  • GitHub Actions workflow
  • Automated tests on PR
  • Automated deployment on push to main
  • Deployment verification

Monitoring

  • CloudWatch dashboard with Golden Signals
  • At least 3 meaningful alarms
  • Logs flowing to CloudWatch
  • Logs Insights queries documented

Documentation

  • Professional README
  • Architecture diagram
  • API documentation
  • Setup instructions
  • .env.example file

Extension Requirements (Complete 1-2)

  • Extension implemented and working
  • Extension documented in README
  • Extension demonstrated in video
  • Extension tests included

Submission Requirements

What to Submit

1. GitHub Repository URL: Public repository with clean commit history (meaningful commit messages), .gitignore excluding secrets, MIT or similar license.

2. Live Deployment URL: Working public endpoint with /health responding, /docs showing API documentation, example request working.

3. Demo Video: 5-10 minutes, uploaded to YouTube (unlisted), link in README.

4. Written Reflection: 2-3 pages, PDF or Markdown in docs/REFLECTION.md, covers all required topics.

5. Coverage Report: HTML coverage report in repo (htmlcov/), badge in README showing percentage, minimum 70% achieved.

Evaluation Rubric

Technical Implementation (60 points)

External API Integration (10 points): All three APIs integrated (10 pts), two APIs working (7 pts), one API or authentication issues (4 pts).

OAuth 2.0 Implementation (10 points): Complete flow with token refresh (10 pts), basic flow without refresh (7 pts), partial or broken implementation (4 pts).

Database Design (10 points): Proper normalization, indexes, migrations (10 pts), working schema with minor issues (7 pts), basic CRUD without optimization (4 pts).

API Quality (10 points): All endpoints working, validated, documented (10 pts), most endpoints working with some validation gaps (7 pts), basic functionality only (4 pts).

Testing (10 points): 70%+ coverage with quality tests (10 pts), 50-69% coverage (7 pts), under 50% or low-quality tests (4 pts).

Deployment (10 points): Full AWS stack working with monitoring (10 pts), deployed but missing monitoring/auto-scaling (7 pts), local only or broken deployment (4 pts).

Professional Practices (25 points)

Code Quality (5 points): Clean, documented, follows patterns (5 pts), functional but inconsistent (3 pts), messy (1 pt).

Git History (5 points): Meaningful commits, logical progression (5 pts), acceptable (3 pts), one giant commit (1 pt).

CI/CD (5 points): Full pipeline with quality gates (5 pts), basic automation (3 pts), manual deployment (1 pt).

Monitoring (5 points): Comprehensive dashboard and alarms (5 pts), basic monitoring (3 pts), none (0 pts).

Security (5 points): OAuth, secrets management, best practices (5 pts), partial (3 pts), major issues (1 pt).

Portfolio Readiness (15 points)

README Quality (5 points): Professional, comprehensive, visual (5 pts), complete but plain (3 pts), basic (1 pt).

Demo Video (5 points): Clear, comprehensive, well-paced (5 pts), functional but rough (3 pts), minimal or unclear (1 pt).

Reflection Insights (5 points): Thoughtful analysis with specific examples (5 pts), generic reflections (3 pts), superficial (1 pt).

Scoring

  • 90-100 points: Excellent. Ready for senior-level interviews.
  • 75-89 points: Good. Ready for interviews with minor polish.
  • 60-74 points: Acceptable. Complete but needs improvement.
  • Under 60 points: Needs revision. Significant gaps remain.

8. Chapter Summary

You did it. You built a complete production API system from architecture through deployment. You integrated three external APIs with different authentication methods. You designed a normalized database schema with proper relationships and indexes. You implemented caching that improved performance by 162x. You deployed to AWS with professional infrastructure. You automated testing and deployment with CI/CD. You configured monitoring and auto-scaling.

This isn't a toy project. This is production-grade infrastructure demonstrating competency that many engineers with years of experience don't have. You made every architectural decision. You debugged every problem. You deployed real infrastructure. You earned this.

Key Skills Mastered

1

Multi-Source API Integration

You integrated three external APIs with completely different patterns: NewsAPI (API key authentication), The Guardian (no authentication), and Reddit (OAuth 2.0 authorization code flow). You normalized their different response formats into a unified schema. You handled rate limits, timeouts, and errors gracefully. You built async clients that fetch from all sources concurrently using asyncio.gather(). Professional APIs aggregate multiple sources—you demonstrated this pattern.

2

Production Database Design

You designed a normalized PostgreSQL schema with proper foreign key relationships, indexes for performance, and constraints for data integrity. You wrote Alembic migrations that version-control your schema changes. You implemented full-text search using GIN indexes and tsvector columns. You handled database connection pooling and session management correctly in an async context. These are the database skills professional engineers use daily.

3

OAuth 2.0 Implementation

You implemented the complete OAuth 2.0 authorization code flow: generating authorization URLs with proper state parameters, exchanging authorization codes for access tokens, storing tokens securely in the database, refreshing expired tokens, and making authenticated requests. You secured the callback endpoint and validated state parameters to prevent CSRF attacks. OAuth is complex—you mastered it.

4

High-Performance Caching

You implemented Redis caching that transformed 700ms multi-API requests into 5ms cached responses (162x improvement). You chose appropriate cache TTLs balancing freshness with performance (5 minutes for news articles). You implemented cache key strategies that prevent collisions. You added cache-aside patterns where your application checks cache before databases. You measured cache hit rates and understood the performance impact. Caching is critical for production systems—you demonstrated mastery.

5

AWS Production Deployment

You deployed to AWS ECS Fargate (managed containers), RDS PostgreSQL (managed database with automated backups), ElastiCache Redis (managed caching), and Application Load Balancer (traffic distribution). You configured security groups restricting access, IAM roles granting permissions, environment variables for configuration, and health checks for availability. You verified everything works in production. This is real infrastructure serving real traffic.

6

CI/CD & Operations

You built a GitHub Actions pipeline that runs tests, builds Docker images, pushes to ECR, and deploys to ECS automatically on every commit. You configured CloudWatch dashboards monitoring the Golden Signals (latency, traffic, errors, saturation). You set up auto-scaling policies that respond to CPU metrics by scaling from 2 to 10 containers. You can deploy multiple times per day confidently because the pipeline validates changes. Professional teams operate this way—you demonstrated operational competency.

Chapter Review Quiz

Test your understanding with these comprehensive questions. If you can answer confidently, you've mastered the material:

Select question to reveal the answer:
Why does the News Intelligence Platform use three different external APIs (NewsAPI, The Guardian, Reddit) instead of just one comprehensive news source?

All three reasons contribute to the architectural decision: The multi-source architecture serves multiple purposes. Pedagogically, it demonstrates handling different authentication patterns in one system (API key authentication, no authentication, and OAuth 2.0). This is crucial learning because production systems rarely have the luxury of uniform interfaces—you integrate whatever data sources provide value. Practically, using multiple sources increases article availability within free tier limits (100/day NewsAPI + 500/day Guardian + 60/min Reddit = much more content). From a product perspective, diversity matters: The Guardian provides high-quality journalism with editorial standards, NewsAPI aggregates from thousands of sources with algorithmic ranking, and Reddit offers community-driven news with discussion. The combination creates a more comprehensive news intelligence platform than any single source could provide. This architectural pattern—integrating multiple heterogeneous sources behind a unified interface—is exactly what professional APIs do.

Your Redis cache has a 5-minute TTL for article searches. A user searches for "climate change" at 10:00 AM, then again at 10:03 AM. What happens on the second search?

The cache returns the 10:00 AM results immediately (~5ms response): The cache-aside pattern works like this: on the first request at 10:00 AM, your application checks Redis, finds no cached value, fetches from all three external APIs (~700ms), stores the combined results in Redis with a 5-minute TTL, and returns the results. On the second request at 10:03 AM (3 minutes later), your application checks Redis, finds the cached value (TTL hasn't expired), and returns it immediately (~5ms response). The TTL countdown started at 10:00 AM, so the cache expires at 10:05 AM. This is the power of caching: repeated identical queries are 140x faster. News articles don't change every second, so serving 3-minute-old results is acceptable. The 5-minute TTL balances freshness (news updates frequently enough) with performance (most users see cached results). Note that accessing a cached value does NOT reset the TTL—the cache expires at 10:05 AM regardless of how many times it's accessed. Some caching strategies use sliding expiration (TTL resets on access), but cache-aside with fixed TTL is simpler and more predictable for news content.

Your ECS auto-scaling policy is configured to scale from 2 to 10 containers when CPU exceeds 70%. During a traffic spike, CPU reaches 75% but no new containers launch. What's the most likely cause?

The CloudWatch alarm hasn't been in ALARM state long enough: Auto-scaling isn't instantaneous. CloudWatch alarms require multiple datapoints above the threshold before transitioning to ALARM state. For example, if your alarm requires 2 datapoints out of 2 evaluation periods with 1-minute intervals, CPU must exceed 70% for 2 consecutive minutes before the alarm triggers. This prevents scaling on brief spikes—a 10-second burst to 75% CPU followed by return to 50% shouldn't launch containers. Once the alarm enters ALARM state, ECS receives the scaling action, which then takes 2-4 minutes to launch new containers (pull image, start task, pass health checks, register with ALB). Total time from "CPU exceeds threshold" to "new container serving traffic" is typically 3-6 minutes. Health check failures prevent containers from receiving traffic but don't prevent launching. The maximum is 10 containers and you're only running 2, so that's not the limit. Cooldown periods exist but are typically 60-300 seconds, and wouldn't prevent the initial scale-out. The most common cause of "scaling didn't happen when I expected" is: not understanding alarm evaluation periods. Production systems must tolerate 3-6 minute scaling lag, which is why you run minimum capacity to handle baseline traffic without auto-scaling.

A user completes Reddit OAuth authorization successfully (you receive an authorization code), but when you try to exchange it for an access token, you get a 401 Unauthorized error. What's the most common cause?

Any of these could cause 401 Unauthorized—OAuth failures are notoriously difficult to debug: OAuth debugging is frustrating because authorization servers return generic 401 errors for many distinct problems. Authorization codes are single-use and expire in 60 seconds—if your code tries to exchange the same code twice (perhaps because of a retry or double-click), the second attempt gets 401. The redirect_uri must match EXACTLY between authorization and token requests—"http://localhost:8000/callback" ≠ "http://localhost:8000/callback/" (trailing slash). Reddit requires Basic auth with base64-encoded client_id:client_secret—malformed encoding or wrong credentials = 401. Other causes include: expired authorization codes (waited too long to exchange), wrong grant_type parameter, or clock skew between servers. The OAuth spec intentionally returns vague errors to prevent information leakage to attackers. Your debugging strategy: (1) Log the complete authorization URL and verify redirect_uri exactly, (2) Test client_id:client_secret auth separately, (3) Exchange codes immediately (don't wait), (4) Never reuse codes. The implementation in Section 3 handles these patterns correctly, but OAuth failures will still happen—expect to spend 1-2 hours debugging OAuth when you first implement it.

Your GitHub Actions CI/CD pipeline runs tests successfully, builds a Docker image, and pushes to ECR. But when ECS pulls the new image and tries to start containers, they fail health checks. Where should you start debugging?

All of the above—production debugging requires systematically checking multiple failure points: Production deployment failures require systematic investigation across multiple layers. Start with CloudWatch Logs (ECS → Task → Logs tab in AWS Console). Containers write stdout/stderr to CloudWatch, so you'll see FastAPI startup logs, database connection errors, or Python exceptions. Common causes: Missing environment variables (DATABASE_URL incorrect or not set), wrong database credentials, incorrect Redis host/port, or application crashes on startup. Next check security groups: if your container can't reach RDS (port 5432) or ElastiCache (port 6379), the application fails health checks even though it started. Verify: ECS security group has outbound rules, RDS/ElastiCache security groups allow inbound from ECS security group. Health check configuration matters too: if your ALB health check path is /health but your application only exposes /healthz, health checks always fail. Debugging strategy: (1) CloudWatch Logs first (application-level errors), (2) Security groups second (network-level errors), (3) Environment variables third (configuration errors), (4) Health check config fourth (monitoring errors). Chapter 29 covers operations—production debugging is a core competency. Tests pass locally but fail in production? Environment differences. Containers fail immediately? Check logs. Containers start but fail health checks? Network or health check config.

You're adding sentiment analysis as an extension (Extension A). When should you analyze sentiment for articles—when fetching from external APIs, when storing in the database, or on-demand when users request sentiment data?

When storing in the database—analyze once per article and store results: Sentiment analysis should happen when articles are first stored in PostgreSQL (the write path) rather than when fetching from external APIs or on-demand when users request data. Here's why: analyzing on the write path means you compute sentiment once per article, store the result in the database, and every subsequent read is fast (just query the database). If you analyzed on-demand, users would wait for TextBlob to process article text on every view—slow and wasteful. If you analyzed when fetching from external APIs, you'd need to analyze the same article multiple times when different users search similar queries, and cached results would include sentiment but cache invalidation becomes complex. Background jobs add latency—articles fetched at 10:00 AM don't get sentiment until the 11:00 AM batch job runs. The write-path approach means: (1) User searches "climate change", (2) Your API fetches from external APIs, (3) For each new article, run sentiment analysis and store in article_sentiments table, (4) Return articles with sentiment data, (5) Next user searching "climate change" gets cached results with sentiment already computed. This pattern—expensive operations on write, fast operations on read—is fundamental to system design. Reads happen more frequently than writes, so optimize the read path. Section 5's implementation correctly analyzes on database insert.

Your README documentation is critical for portfolio presentation. Which of these sections is MOST important for demonstrating your project's value to recruiters and hiring managers?

Architecture diagram and system design explanation—showing how you designed the system: All README sections matter, but the architecture/system design section is MOST valuable for demonstrating competency. Here's why: recruiters spend 30-60 seconds per GitHub repository before deciding whether to dig deeper. In that time, they're evaluating: "Does this person think like an engineer?" A clear architecture diagram with component explanations immediately communicates: this person designs systems, not just writes code. Your diagram shows: external APIs, your FastAPI application, PostgreSQL, Redis, AWS infrastructure, and how they interact. The accompanying text explains WHY you made each decision: "I chose PostgreSQL over MongoDB because foreign key relationships fit the domain naturally" or "Redis caching transforms 700ms requests into 5ms responses." This demonstrates systems thinking. Installation instructions are necessary but don't differentiate you—anyone can run npm install. API documentation is valuable but shows implementation detail, not design thinking. Demo videos are impressive but recruiters may not watch 5 minutes—they'll skim README first. The architecture section is your elevator pitch: it tells recruiters immediately that you understand system design, performance optimization, infrastructure deployment, and trade-off analysis. Combined with the live deployed URL and metrics, your architecture section turns "student project" into "professional portfolio piece." Section 6 provides templates for documentation—use them.

During your interview for a Backend Engineering role, you're asked: "How would you scale this News Intelligence Platform to handle 10x the traffic?" What's the best approach to answering this system design question?

Start by clarifying requirements, discuss multiple bottlenecks, explain trade-offs: System design interview questions test your thinking process, not your ability to propose clever solutions. The best answers start with clarification: "When you say 10x traffic, do you mean 10x read requests (article searches), 10x writes (new articles being ingested), or both?" This shows you understand different scaling challenges. Then systematically identify bottlenecks: "Currently, the database handles 100 queries/second. At 10x, we'd hit PostgreSQL connection limits around 900 qps. The solution depends on the query pattern—if it's mostly reads, read replicas with query routing would work. If it's writes, we'd need to partition the articles table by date or source." Discuss trade-offs: "Adding read replicas is simpler than partitioning but doesn't help write throughput. Partitioning helps writes but complicates queries that span partitions." Reference your actual implementation: "My current architecture auto-scales ECS containers from 2 to 10. That handles compute load, but the database is the next bottleneck. I'd add RDS read replicas first (easiest win), then optimize query patterns, then consider caching more aggressively." Describing just horizontal scaling is fine but shallow—doesn't explore trade-offs. Over-engineering with microservices and Kubernetes isn't needed for 10x scale. Referencing metrics is good but incomplete. The best answer demonstrates: you ask clarifying questions, you identify bottlenecks systematically, you evaluate multiple solutions, you explain trade-offs, you reference actual metrics. This is how staff engineers approach scaling challenges. Practice this question—"how would you scale X"—because it's asked in 80% of backend interviews.

9. What's Next?

You've built something significant. A production-grade API deployed on AWS with professional operations. This demonstrates competency that many developers with years of experience don't have. But learning doesn't stop here.

Beyond the Capstone

If You Want to Keep Building This Project

Multi-region deployment: Deploy your API to multiple AWS regions (us-east-1, eu-west-1, ap-southeast-1). Use Route 53 geolocation routing to direct users to the nearest region. Implement database replication between regions for disaster recovery.

GraphQL API layer: Add a GraphQL endpoint alongside your REST API. Users can request exactly the fields they need. Use Strawberry or Graphene libraries. This demonstrates polyglot API design.

Mobile app: Build a React Native app consuming your API. Push notifications for new articles. Offline caching. iOS and Android from a single codebase. Now you have a full-stack portfolio.

Machine learning improvements: Replace TextBlob with a fine-tuned BERT model for sentiment analysis. Train on news-specific datasets. Deploy model using AWS SageMaker. Document the improvement in accuracy.

Serverless components: Add AWS Lambda functions for specific tasks like thumbnail generation, email sending, or webhook processing. Show hybrid architecture (containers plus serverless).

Social features: Add commenting, article sharing, user following. Build a social layer on top of news aggregation. Demonstrates complex database relationships and social graph algorithms.

Open Source Contribution Ideas

Contributing to open source demonstrates professional collaboration skills:

FastAPI ecosystem: Create a FastAPI extension for rate limiting, caching, or metrics. Submit as PyPI package. Maintainers often highlight quality contributions.

SQLAlchemy patterns: Document common patterns you discovered (async session management, migration strategies). Write a blog post or create example repository.

AWS CDK constructs: Convert your infrastructure to AWS CDK (Infrastructure as Code). Publish reusable constructs for common patterns (ECS plus RDS plus ElastiCache).

Testing libraries: Create pytest fixtures for common FastAPI testing patterns. Share utilities that make testing APIs easier.

Interview Preparation

Technical Interview Questions You Can Answer

System Design: "Design a news aggregation system that scales to millions of users." "How would you implement caching in a distributed system?" "Design an OAuth authentication flow." You built this. You can walk through every decision.

Behavioral Questions: "Tell me about a challenging technical problem you solved." "Describe a system you built from scratch." "How do you approach debugging production issues?" Use your capstone experiences. Actual examples from real infrastructure.

Technical Deep-Dives: "Explain how you'd debug a memory leak in production." "How does auto-scaling work and what are the trade-offs?" "Walk me through your CI/CD pipeline." You operated this system. You have concrete answers.

Your 30-Second Pitch

Practice This Until Natural

"I built a production news aggregation platform deployed on AWS that integrates three external APIs with different authentication methods: NewsAPI with API keys, Guardian with no auth, and Reddit with OAuth 2.0. The system uses PostgreSQL for persistent storage, Redis for caching, achieving 162x performance improvements on cached requests. I deployed it to AWS ECS Fargate with auto-scaling from 2 to 10 containers based on CPU metrics, implemented GitHub Actions for CI/CD with automated testing and deployment, and built CloudWatch dashboards monitoring the four Golden Signals. The entire system handles 500+ requests per second with sub-10ms response times on cached queries."

Talking About Trade-Offs

Interviewers want to see you think critically about decisions. Example: "Why did you use PostgreSQL instead of MongoDB?" Strong answer: "I chose PostgreSQL because the domain has clear relationships. Users own articles, articles have sentiments. PostgreSQL's foreign keys enforce referential integrity at the database level. I also needed full-text search, which PostgreSQL provides natively with tsvector columns and GIN indexes. MongoDB would require application-level relationship management and likely ElasticSearch for search, adding complexity. For this use case, a relational model fit naturally."

Not: "I used PostgreSQL because the book said to."

Career Resources

Where to Apply This Knowledge

Backend Engineering Roles: API Development Engineer, Platform Engineer, Backend Software Engineer, Cloud Infrastructure Engineer, Site Reliability Engineer (SRE). Your project demonstrates: API design, database architecture, caching strategies, cloud deployment, monitoring, CI/CD.

Data Engineering Roles: Data Pipeline Engineer, ML Infrastructure Engineer, Analytics Engineer. Your project demonstrates: ETL patterns (fetching, transforming, storing data), database optimization, scheduled jobs, data quality.

Full-Stack Roles: Full-Stack Developer, Product Engineer, Startup Engineer. Your project demonstrates: Backend mastery with room to add frontend. Shows ability to own features end-to-end.

Continuing Education

Books: Designing Data-Intensive Applications by Martin Kleppmann (system design bible), Site Reliability Engineering by Google (SRE practices), The Phoenix Project by Gene Kim (DevOps culture).

Online Courses: System Design Interview courses (Educative, Exponent, ByteByteGo), Kubernetes (Cloud Native Computing Foundation), Advanced AWS (AWS Skill Builder).

Practice Platforms: LeetCode (algorithms and system design premium), HackerRank (skills assessment), ExecuTech (mock interviews).

Building Your Online Presence

Technical Blog: Write about what you built. Potential topics: "How I Optimized API Response Times from 700ms to 4ms with Redis," "Implementing OAuth 2.0 in FastAPI: A Complete Guide," "Auto-Scaling on AWS: Configuration, Testing, and Gotchas."

GitHub Profile: Pin your best repositories (this capstone!), write good commit messages, maintain active contributions (green squares), follow interesting developers.

LinkedIn: Add this project to experience section, write posts about what you learned, connect with engineers at companies you admire, join relevant groups (AWS, Python, DevOps).

Final Words

You did it. You built a complete production system from scratch. You integrated external APIs, designed databases, implemented authentication, containerized applications, deployed to AWS, automated operations, and monitored production systems. These aren't theoretical skills. You have running infrastructure demonstrating competency.

This project is your proof. When recruiters ask "Can you build production APIs?", you have a deployed system answering yes. When they ask about debugging, scaling, monitoring, you have real experiences. Most bootcamp graduates don't have this depth. Many developers with years of experience have never deployed to AWS or built CI/CD pipelines.

Keep building. This capstone is a beginning, not an end. The patterns you learned apply to any system: start with requirements, design architecture, implement incrementally, test thoroughly, deploy professionally, monitor continuously, optimize iteratively. That cycle works for everything from toy projects to billion-dollar systems.

You're ready. Apply for roles. Do technical interviews. Build more projects. Contribute to open source. Write technical content. The path from here is yours to choose.

Most importantly: Be proud of what you built. You earned it.

Congratulations

Congratulations on completing the News Intelligence Platform capstone project. You're a production-ready engineer.