📊 Developer Cookbook - FASE 8: Estimación y Gestión de Proyectos

Story points, planning poker, gestión de riesgos y comunicación de incertidumbre


Receta 8.9: Story Points y Planning Poker

¿Qué son?

Por qué Story Points > Horas:

class AgileEstimation:
    """Framework para estimación ágil"""

    @staticmethod
    def why_story_points():
        """Por qué usar story points en vez de horas"""

        # PROBLEM WITH HOUR ESTIMATES:
        # - Different developers, different speeds:
        #   Junior: "16 hours" | Senior: "2 hours" (both correct, not comparable)
        # - Same developer, different days:
        #   Monday focused: 4 hours | Friday tired: 8 hours
        # Hours vary. Complexity doesn't.

        # STORY POINTS MEASURE:
        # - Complexity (How hard?)
        # - Uncertainty (How much unknown?)
        # - Effort (How much work?)
        # NOT time!

        # FIBONACCI SCALE: 1, 2, 3, 5, 8, 13, 21, 34, 55, 89
        # Why Fibonacci? Bigger gaps for bigger tasks (intentional vagueness)

        calibration = {
            "1 point (Trivial)": [
                "Fix typo in documentation",
                "Update environment variable",
                "Change button color"
            ],
            "2 points (Simple)": [
                "Add logging to existing function",
                "Write unit test for existing code",
                "Update copy on a page"
            ],
            "3 points (Straightforward)": [
                "Add new API endpoint (CRUD)",
                "Create simple form with validation",
                "Fix minor bug with clear reproduction"
            ],
            "5 points (Moderate)": [
                "Build feature with 2-3 components",
                "Refactor module",
                "Fix bug requiring investigation"
            ],
            "8 points (Complex)": [
                "Multi-step workflow",
                "Integration with third-party API",
                "Performance optimization"
            ],
            "13 points (Very Complex)": [
                "Large feature touching many modules",
                "Significant refactor",
                "Unknown complexity (needs spike)"
            ],
            "21+ points (Epic)": [
                "Too large, needs to be broken down",
                "Multiple sprints"
            ]
        }

        # Rule: If >13 points, split the story

        return calibration

    @staticmethod
    def planning_poker_process():
        """Cómo jugar planning poker"""

        process = """
        PLANNING POKER PROCESS
        ======================

        SETUP:
        - Everyone gets cards: 1, 2, 3, 5, 8, 13, 21, ?, coffee
        - Product Owner presents story
        - Timer ready (optional)

        STEPS:

        1. PRESENT STORY (5 min)
           PO: "As a user, I want to reset my password via email"
           Acceptance criteria:
           - User clicks "Forgot password"
           - Enter email
           - Receive email with reset link
           - Click link, set new password

        2. CLARIFY (5 min)
           Team asks questions:
           Dev 1: "Do we already have email service?"
           PO: "Yes, SendGrid is set up"
           Dev 2: "How long is reset link valid?"
           PO: "24 hours"

        3. ESTIMATE INDIVIDUALLY (1 min)
           - Each person thinks independently
           - Choose a card
           - Place face-down (don't show yet!)

        4. REVEAL SIMULTANEOUSLY
           Everyone flips cards at once:
           Alice: 3 | Bob: 5 | Charlie: 8 | Diana: 5

        5. DISCUSS OUTLIERS (5 min)
           Facilitator: "Alice, why 3?"
           Alice: "Seems straightforward. Email service exists."

           Facilitator: "Charlie, why 8?"
           Charlie: "We need to add password validation rules,
           test token expiration, handle edge cases. Security concerns too."

           Team: "Oh right, we forgot about security!"

        6. RE-ESTIMATE
           Alice: 5 | Bob: 5 | Charlie: 5 | Diana: 5
           Consensus! Story = 5 points

        7. REPEAT FOR NEXT STORY

        SPECIAL CARDS:
        ? (Question mark) = "I don't understand the story / need more info"
        coffee cup = "I'm tired, need a break"
        Infinity = "This is way too big, needs to be split"

        TIPS:
        - Estimate as team (not individually)
        - Discuss outliers (high and low)
        - Use previous stories as reference
        - Time-box discussions (max 10 min per story)
        - Never show cards before everyone is ready (anchoring bias)
        - Never average estimates (defeats purpose)
        """
        print(process)

    @staticmethod
    def velocity_and_capacity():
        """Calcular velocity y capacity"""

        # VELOCITY: Points completed per sprint (historical data)
        # Sprint 1: 23 | Sprint 2: 28 | Sprint 3: 25 | Sprint 4: 20 | Sprint 5: 27
        # Average velocity = (23+28+25+20+27) / 5 = 24.6 ~= 25 points

        # CAPACITY: Available hours per sprint
        # Example (2-week sprint, 4 developers):
        # Theoretical: 4 * 10 * 8 = 320 hours
        # Subtract meetings (80h) + code reviews (40h) + slack (40h) + PTO (40h) + buffer 10%
        # Actual capacity: ~88 hours

        # COMMITMENT:
        # Conservative: use lowest recent velocity (20 points)
        # Moderate: use average velocity (25 points)
        # Aggressive: use highest (28 points) - risky

        # BURN-DOWN CHART:
        # Day 0: 25 points | Day 2: 22 | Day 4: 18 | Day 6: 15 | Day 8: 10 | Day 10: 0
        # Ideal: Straight line from start to end
        # Too slow -> drop lowest priority stories or ask for help
        # Too fast -> pull in more stories from backlog

        # FACTORS AFFECTING VELOCITY:
        # Increase: reduce tech debt, improve test automation, clearer requirements
        # Decrease: new team members, more meetings, production incidents

        pass

AgileEstimation.planning_poker_process()

Receta 8.10: Gestión de Riesgos Técnicos

¿Qué es? Identificar y mitigar riesgos técnicos antes de que se conviertan en problemas.

Framework de gestión de riesgos:

from dataclasses import dataclass
from typing import List
from enum import Enum

class RiskProbability(Enum):
    """Probabilidad de que ocurra"""
    LOW = 1      # <20% chance
    MEDIUM = 2   # 20-50% chance
    HIGH = 3     # >50% chance

class RiskImpact(Enum):
    """Impacto si ocurre"""
    LOW = 1      # Minor inconvenience
    MEDIUM = 2   # Delays sprint
    HIGH = 3     # Delays release
    CRITICAL = 4 # Blocks launch

@dataclass
class TechnicalRisk:
    """Modelo de riesgo técnico"""

    title: str
    description: str
    probability: RiskProbability
    impact: RiskImpact
    triggers: List[str]    # Señales de que está ocurriendo
    mitigation: List[str]  # Cómo prevenirlo
    contingency: List[str] # Qué hacer si ocurre
    owner: str

    def risk_score(self) -> int:
        """Calcular score de riesgo (probability * impact)"""
        return self.probability.value * self.impact.value

    def priority(self) -> str:
        """Prioridad basada en score"""
        score = self.risk_score()
        if score >= 9:
            return "CRITICAL"
        elif score >= 6:
            return "HIGH"
        elif score >= 3:
            return "MEDIUM"
        else:
            return "LOW"

# Ejemplo: Riesgos de un proyecto de migración de DB
risks = [
    TechnicalRisk(
        title="Data loss during migration",
        description="Migrating 100M records from MySQL to PostgreSQL. Risk of data corruption or loss.",
        probability=RiskProbability.MEDIUM,
        impact=RiskImpact.CRITICAL,
        triggers=[
            "Test migration shows missing records",
            "Checksums don't match",
            "Users report missing data"
        ],
        mitigation=[
            "Test migration on staging with production snapshot",
            "Row-by-row checksum verification",
            "Dual-write to both DBs during transition",
            "Keep MySQL running for 1 month as backup"
        ],
        contingency=[
            "Rollback to MySQL immediately",
            "Restore from backup",
            "Communicate: 'Service degraded, investigating'"
        ],
        owner="@sarah-backend"
    ),

    TechnicalRisk(
        title="Migration takes longer than expected",
        description="Estimated 4 hours, but could take 12+ hours if issues arise.",
        probability=RiskProbability.HIGH,
        impact=RiskImpact.HIGH,
        triggers=[
            "Test migration took 6 hours (50% slower than estimate)",
            "Network bandwidth lower than expected",
            "Lock contention on tables"
        ],
        mitigation=[
            "Schedule during low-traffic window (3am Sunday)",
            "Batch migration in chunks (10M rows at a time)",
            "Pre-warm new database",
            "Optimize queries before migration"
        ],
        contingency=[
            "Extend maintenance window",
            "Pause migration, resume later",
            "Rollback and reschedule"
        ],
        owner="@devops-lead"
    ),

    TechnicalRisk(
        title="Application doesn't work with PostgreSQL",
        description="Despite testing, some edge cases might only work with MySQL.",
        probability=RiskProbability.LOW,
        impact=RiskImpact.HIGH,
        triggers=[
            "Errors in production logs after migration",
            "Users reporting bugs",
            "Features not working as expected"
        ],
        mitigation=[
            "Comprehensive integration tests on staging",
            "Canary deployment (1% of traffic first)",
            "Feature flags to toggle between MySQL/PostgreSQL",
            "Shadow traffic (compare responses from both DBs)"
        ],
        contingency=[
            "Route traffic back to MySQL",
            "Hotfix and redeploy",
            "Rollback migration"
        ],
        owner="@sarah-backend"
    ),
]

# Risk Register
class RiskRegister:
    """Registro de riesgos del proyecto"""

    def __init__(self, risks: List[TechnicalRisk]):
        self.risks = risks

    def print_register(self):
        """Imprimir registro de riesgos ordenado por prioridad"""
        sorted_risks = sorted(self.risks, key=lambda r: r.risk_score(), reverse=True)

        print("TECHNICAL RISK REGISTER")
        print("=" * 80)

        for risk in sorted_risks:
            print(f"\n[{risk.priority()}] {risk.title}")
            print(f"Owner: {risk.owner}")
            print(f"Score: {risk.risk_score()}/12")
            print(f"Description: {risk.description}")

            print("Triggers:")
            for t in risk.triggers:
                print(f"  - {t}")

            print("Mitigation:")
            for m in risk.mitigation:
                print(f"  - {m}")

            print("Contingency:")
            for c in risk.contingency:
                print(f"  - {c}")
            print("-" * 80)

register = RiskRegister(risks)
register.print_register()

Risk review meetings:

# Weekly Risk Review Template

**Date**: 2024-02-20
**Project**: Database Migration
**Attendees**: @sarah, @devops-lead, @tech-lead, @pm

## Agenda
1. Review existing risks (15 min)
2. Identify new risks (10 min)
3. Update mitigation plans (10 min)
4. Assign actions (5 min)

## Risk Updates

### CRITICAL: Data loss during migration
**Status**: MITIGATED
- Completed test migration on staging
- 100% checksum match
- Dual-write setup complete
- Keeping MySQL for 1 month (in progress)

**Next actions**:
- [ ] Final production migration dry-run (Saturday 3am) - @sarah

### HIGH: Migration takes too long
**Status**: IN PROGRESS
- Scheduled 6-hour window (3am-9am Sunday)
- Batching implemented (10M rows/batch)
- Network bandwidth: only 500Mbps (expected 1Gbps)

**Concern**: Bandwidth might extend migration to 8 hours

**Actions**:
- [ ] Compress data before transfer - @devops-lead
- [ ] Request ISP upgrade (emergency) - @devops-lead
- [ ] Prepare comms for extended window - @pm

### New Risk Identified
**Title**: Third-party API rate limits
**Probability**: LOW | **Impact**: MEDIUM
**Description**: 1M API calls/day. PostgreSQL setup might change
request patterns and trigger rate limits.

**Mitigation**:
- [ ] Monitor API usage during migration - @sarah
- [ ] Contact API provider for temp limit increase - @pm

## Action Items
- [ ] Network bandwidth upgrade - @devops-lead - Due: Feb 22
- [ ] Production dry-run - @sarah - Due: Feb 24
- [ ] Extended window communication draft - @pm - Due: Feb 23

## Next Review: Feb 27, 2pm

Receta 8.11: Comunicación de Incertidumbre

¿Qué es? Comunicar estimaciones y riesgos de forma honesta sin sonar incompetente o crear pánico.

Framework para comunicar incertidumbre:

class UncertaintyCommunication:
    """Cómo comunicar estimaciones e incertidumbre"""

    @staticmethod
    def cone_of_uncertainty():
        """Explicar el cono de incertidumbre"""

        # Early in project: Wide uncertainty
        # Late in project: Narrow uncertainty
        #
        # Early project:  "Could be 2 weeks or 8 weeks" (4x variance)
        # After design:   "Will be 3-5 weeks" (1.5x variance)
        # After impl:     "Will be 3.5-4 weeks" (1.1x variance)
        #
        # BAD (over-confident):
        # "This will take exactly 3 weeks" (Day 1, no design yet)
        #
        # GOOD (acknowledges uncertainty):
        # "Based on similar projects, this is 2-8 weeks.
        #  After we finish design phase next week,
        #  I can give you a tighter estimate."
        #
        # PM: "Can you commit to 3 weeks?"
        # BAD: "Yes" (sets false expectation)
        # GOOD: "I can commit to getting you a confident estimate
        #        after design is done. Right now, best guess is
        #        3-6 weeks, but that could change once we understand
        #        the requirements better."
        pass

    @staticmethod
    def estimation_templates():
        """Templates para diferentes niveles de confianza"""

        templates = {
            "HIGH CONFIDENCE (done similar before)": """
                "This is 3-4 days of work. I've built similar
                features and don't foresee surprises. I'm 80%
                confident we'll hit this."
                Components: Range + Context (why confident) + Confidence level
            """,

            "MEDIUM CONFIDENCE (some unknowns)": """
                "This is probably 1-2 weeks. The core work is
                straightforward, but we need to integrate with
                the payment API which I haven't used before.
                That could add a few days if their docs are unclear."
                Components: Range + What's known + What's unknown + What could go wrong
            """,

            "LOW CONFIDENCE (many unknowns)": """
                "I honestly don't know yet. This requires research
                into the legacy codebase which is poorly documented.
                Give me 2 days to spike it, and I can give you a
                real estimate. Right now, gut feeling is 1-4 weeks,
                but that's low confidence."
                Components: Honesty + What you need to reduce uncertainty + Rough ballpark
            """,

            "IMPOSSIBLE TO ESTIMATE": """
                "I can't estimate this without breaking it down first.
                The requirements are too vague. Can we schedule a
                design session to clarify scope? After that, I can
                give you an estimate."
                Components: Clear 'I can't' + Why not + What's needed + When you can estimate
            """,

            "RESPONDING TO DEADLINE": """
                PM: "We need this by March 1. Can you do it?"

                BAD: "I'll try" / "Maybe" / "Yes" (when you mean no)

                Option A (Yes, with caveat):
                "Yes, if we cut features X and Y. The full scope
                is 6 weeks, but MVP is doable in 4 weeks."

                Option B (No, with alternative):
                "No, realistic estimate is 6 weeks. We could hit
                March 1 with 2 more engineers, or phased rollout
                (core March 1, rest March 15)."

                Option C (Need more info):
                "I need to understand requirements better. Give me
                1 day to break this down."
            """,

            "PROJECT IS BEHIND SCHEDULE": """
                BAD: "Everything's fine" (when it's not)
                BAD: "We're totally screwed!"

                GOOD (proactive communication):
                "Update: We're behind schedule. Originally estimated
                2 weeks, we're on day 12 and 60% done.

                Why we're behind:
                - Third-party API was down for 2 days
                - Requirements changed mid-sprint

                New estimate: 4 more days (total 16 days)

                Options:
                1. Extend deadline by 4 days
                2. Cut feature Z (saves 2 days, hit deadline)
                3. Add 1 more dev (might save 1-2 days)

                Recommendation: Option 2 (cut feature Z)"
            """
        }
        return templates

    @staticmethod
    def three_point_estimation():
        """Técnica de estimación de 3 puntos"""

        # Instead of single estimate, give 3:
        # - OPTIMISTIC (Best case): Everything goes perfectly
        # - REALISTIC (Most likely): Normal case
        # - PESSIMISTIC (Worst case): Murphy's law
        #
        # Example: "Add user authentication"
        # Optimistic: 3 days (no blockers, library works, no edge cases)
        # Realistic:  5 days (some debugging, a few edge cases)
        # Pessimistic: 10 days (library bugs, security review, requirements change)
        #
        # PERT Formula:
        # Expected time = (Optimistic + 4*Realistic + Pessimistic) / 6
        # = (3 + 4*5 + 10) / 6 = 33/6 = 5.5 days
        #
        # Communicate as:
        # "Most likely 5 days, could be as low as 3 or as high as 10"
        #
        # When to use: High-stakes projects, many unknowns, need to communicate risk
        # When NOT: Simple tasks, tight deadlines
        pass

    @staticmethod
    def confidence_intervals():
        """Comunicar con intervalos de confianza"""

        examples = [
            "High confidence: 'I'm 90% sure this is 2-3 days' (narrow range)",
            "Medium confidence: 'I'm 70% sure this is 1-2 weeks' (medium range)",
            "Low confidence: 'I'm 50% sure this is 2-8 weeks' (wide range)"
        ]

        # When stakeholder pushes back:
        # PM: "Can't you be more specific than '1-2 weeks'?"
        #
        # BAD:  "Fine, 1.5 weeks" (false precision)
        # GOOD: "I could say 1.5 weeks, but that would be lying.
        #        The uncertainty is real. If you need more certainty,
        #        I can spend 1 day doing a technical spike to narrow the range."

        return examples

UncertaintyCommunication.estimation_templates()

¡Felicidades! 🎉

Has completado la FASE 8: Habilidades Blandas y Liderazgo del roadmap.

Lo que has aprendido:

Comunicación Técnica:

Liderazgo y Mentoría:

Estimación y Gestión de Proyectos:

Próximos pasos:

FASE 9: Meta-aprendizaje — Aprendizaje Continuo, Adaptabilidad, Networking


Versión: 1.0 | Fecha: 2024 | Licencia: Uso educativo