Skip to content
Back to Blog
1 min read

Developer Experience: Making Developers Productive and Happy

Measuring Developer Experience

from dataclasses import dataclass
from typing import Dict, List, Optional
from datetime import datetime, timedelta
import statistics

@dataclass
class DeveloperMetrics:
    """Track developer productivity metrics"""
    developer_id: str
    period_start: datetime
    period_end: datetime

    # DORA metrics
    deployment_frequency: float  # Deploys per day
    lead_time_hours: float  # Commit to production
    change_failure_rate: float  # % of deployments causing issues
    mttr_hours: float  # Mean time to recover

    # Developer satisfaction
    satisfaction_score: Optional[float] = None  # 1-10
    tool_satisfaction: Optional[Dict[str, float]] = None

class DevExDashboard:
    """Developer experience dashboard"""

    def __init__(self, metrics_store):
        self.metrics_store = metrics_store

    def calculate_dora_metrics(
        self,
        team: str,
        period_days: int = 30
    ) -> dict:
        """Calculate DORA metrics for a team"""
        end_date = datetime.utcnow()
        start_date = end_date - timedelta(days=period_days)

        deployments = self.metrics_store.get_deployments(team, start_date, end_date)
        incidents = self.metrics_store.get_incidents(team, start_date, end_date)

        return {
            'team': team,
            'period': f'{period_days} days',
            'deployment_frequency': len(deployments) / period_days,
            'lead_time': self._calculate_lead_time(deployments),
            'change_failure_rate': self._calculate_cfr(deployments, incidents),
            'mttr': self._calculate_mttr(incidents),
            'performance_level': self._classify_performance({
                'deployment_frequency': len(deployments) / period_days,
                'lead_time': self._calculate_lead_time(deployments),
                'change_failure_rate': self._calculate_cfr(deployments, incidents),
                'mttr': self._calculate_mttr(incidents)
            })
        }

    def _classify_performance(self, metrics: dict) -> str:
        """Classify team performance level"""
        # Based on DORA research
        if (metrics['deployment_frequency'] >= 1 and
            metrics['lead_time'] < 24 and
            metrics['change_failure_rate'] < 0.15 and
            metrics['mttr'] < 1):
            return 'elite'
        elif (metrics['deployment_frequency'] >= 0.14 and  # Weekly
              metrics['lead_time'] < 168 and  # Week
              metrics['change_failure_rate'] < 0.30):
            return 'high'
        elif (metrics['deployment_frequency'] >= 0.033 and  # Monthly
              metrics['lead_time'] < 720):  # Month
            return 'medium'
        else:
            return 'low'

    def get_friction_points(self, team: str) -> List[dict]:
        """Identify developer friction points"""
        friction_points = []

        # Build time analysis
        build_times = self.metrics_store.get_build_times(team)
        if statistics.mean(build_times) > 600:  # > 10 minutes
            friction_points.append({
                'category': 'build',
                'issue': 'Slow build times',
                'current': f'{statistics.mean(build_times)/60:.1f} minutes average',
                'target': '< 5 minutes',
                'impact': 'high'
            })

        # Test flakiness
        flaky_tests = self.metrics_store.get_flaky_tests(team)
        if len(flaky_tests) > 0:
            friction_points.append({
                'category': 'testing',
                'issue': f'{len(flaky_tests)} flaky tests',
                'tests': flaky_tests[:5],
                'impact': 'medium'
            })

        # Environment issues
        env_issues = self.metrics_store.get_environment_issues(team)
        if env_issues['setup_time_hours'] > 4:
            friction_points.append({
                'category': 'environment',
                'issue': 'Slow environment setup',
                'current': f'{env_issues["setup_time_hours"]} hours',
                'target': '< 1 hour',
                'impact': 'high'
            })

        return friction_points

Developer Onboarding

# Automated onboarding workflow
apiVersion: platform.company.com/v1
kind: OnboardingWorkflow
metadata:
  name: developer-onboarding
spec:
  steps:
    - name: identity-setup
      description: Create accounts and access
      tasks:
        - Create Azure AD account
        - Add to appropriate security groups
        - Generate SSH keys
        - Setup MFA

    - name: repository-access
      description: Grant repository access
      tasks:
        - Add to GitHub organization
        - Grant access to team repositories
        - Configure Git signing keys

    - name: development-environment
      description: Setup development environment
      tasks:
        - Provision cloud development environment
        - Install standard toolchain
        - Configure IDE settings
        - Setup local Kubernetes cluster

    - name: access-provisioning
      description: Provision service access
      tasks:
        - Azure subscription access
        - Kubernetes cluster access
        - Database read replicas
        - Monitoring dashboards

    - name: training-resources
      description: Assign training materials
      tasks:
        - Platform documentation
        - Team-specific guides
        - Architecture overview
        - Security training

  notifications:
    - type: email
      template: welcome-email
      timing: start
    - type: slack
      channel: new-hires
      timing: complete

Local Development Environment

# Development environment configuration
from dataclasses import dataclass
from typing import List, Optional
import subprocess
import os

@dataclass
class ServiceDependency:
    name: str
    type: str  # 'container', 'mock', 'cloud'
    config: dict

@dataclass
class DevEnvironment:
    """Local development environment configuration"""
    services: List[ServiceDependency]
    environment_variables: dict
    port_mappings: dict

class LocalDevManager:
    """Manage local development environment"""

    def __init__(self, project_root: str):
        self.project_root = project_root
        self.config = self._load_config()

    def _load_config(self) -> DevEnvironment:
        """Load dev environment configuration"""
        import yaml
        config_path = os.path.join(self.project_root, '.devenv.yaml')
        with open(config_path) as f:
            config = yaml.safe_load(f)
        return DevEnvironment(**config)

    def start(self):
        """Start local development environment"""
        print("Starting development environment...")

        # Start dependencies with Docker Compose
        compose_file = self._generate_compose_file()
        subprocess.run(['docker-compose', '-f', compose_file, 'up', '-d'])

        # Wait for services to be healthy
        self._wait_for_services()

        # Apply database migrations
        self._run_migrations()

        # Seed test data
        self._seed_data()

        print("Development environment ready!")
        self._print_service_urls()

    def _generate_compose_file(self) -> str:
        """Generate Docker Compose file for dependencies"""
        services = {}

        for dep in self.config.services:
            if dep.type == 'container':
                services[dep.name] = {
                    'image': dep.config['image'],
                    'ports': dep.config.get('ports', []),
                    'environment': dep.config.get('environment', {}),
                    'volumes': dep.config.get('volumes', []),
                    'healthcheck': dep.config.get('healthcheck', {})
                }
            elif dep.type == 'mock':
                services[dep.name] = self._create_mock_service(dep)

        compose = {
            'version': '3.8',
            'services': services,
            'networks': {
                'devnet': {'driver': 'bridge'}
            }
        }

        import yaml
        compose_path = os.path.join(self.project_root, '.docker-compose.dev.yaml')
        with open(compose_path, 'w') as f:
            yaml.dump(compose, f)

        return compose_path

    def _create_mock_service(self, dep: ServiceDependency) -> dict:
        """Create mock service configuration"""
        return {
            'image': 'mockserver/mockserver:latest',
            'ports': [f"{dep.config['port']}:1080"],
            'environment': {
                'MOCKSERVER_INITIALIZATION_JSON_PATH': '/config/init.json'
            },
            'volumes': [
                f"{self.project_root}/mocks/{dep.name}:/config"
            ]
        }


# .devenv.yaml example
"""
services:
  - name: postgres
    type: container
    config:
      image: postgres:13
      ports:
        - "5432:5432"
      environment:
        POSTGRES_PASSWORD: devpassword
        POSTGRES_DB: appdb
      healthcheck:
        test: ["CMD", "pg_isready", "-U", "postgres"]
        interval: 5s
        timeout: 5s
        retries: 5

  - name: redis
    type: container
    config:
      image: redis:6
      ports:
        - "6379:6379"

  - name: external-api
    type: mock
    config:
      port: 8081
      responses:
        - path: /api/users
          method: GET
          response:
            status: 200
            body: {"users": []}

environment_variables:
  DATABASE_URL: postgresql://postgres:devpassword@localhost:5432/appdb
  REDIS_URL: redis://localhost:6379
  EXTERNAL_API_URL: http://localhost:8081

port_mappings:
  app: 8080
  debug: 5005
"""

Documentation as Code

from typing import List, Optional
import ast
import os

class DocumentationGenerator:
    """Generate documentation from code and annotations"""

    def generate_api_docs(self, source_dir: str) -> str:
        """Generate API documentation from code"""
        endpoints = []

        for root, dirs, files in os.walk(source_dir):
            for file in files:
                if file.endswith('.py'):
                    filepath = os.path.join(root, file)
                    endpoints.extend(self._extract_endpoints(filepath))

        return self._render_api_docs(endpoints)

    def _extract_endpoints(self, filepath: str) -> List[dict]:
        """Extract API endpoints from Python file"""
        with open(filepath) as f:
            tree = ast.parse(f.read())

        endpoints = []
        for node in ast.walk(tree):
            if isinstance(node, ast.FunctionDef):
                for decorator in node.decorator_list:
                    if self._is_route_decorator(decorator):
                        endpoint = self._parse_endpoint(node, decorator)
                        if endpoint:
                            endpoints.append(endpoint)

        return endpoints

    def _render_api_docs(self, endpoints: List[dict]) -> str:
        """Render API documentation as Markdown"""
        doc = "# API Documentation\n\n"

        for endpoint in sorted(endpoints, key=lambda x: x['path']):
            doc += f"## {endpoint['method']} {endpoint['path']}\n\n"
            doc += f"{endpoint['description']}\n\n"

            if endpoint.get('parameters'):
                doc += "### Parameters\n\n"
                doc += "| Name | Type | Required | Description |\n"
                doc += "|------|------|----------|-------------|\n"
                for param in endpoint['parameters']:
                    doc += f"| {param['name']} | {param['type']} | {param['required']} | {param['description']} |\n"
                doc += "\n"

            if endpoint.get('response'):
                doc += "### Response\n\n"
                doc += f"```json\n{endpoint['response']}\n```\n\n"

        return doc


class RunbookGenerator:
    """Generate runbooks from incident history and procedures"""

    def generate_runbook(self, service: str, incident_type: str) -> str:
        """Generate runbook for common incidents"""

        templates = {
            'high_cpu': self._high_cpu_runbook,
            'high_memory': self._high_memory_runbook,
            'high_error_rate': self._high_error_rate_runbook,
            'service_down': self._service_down_runbook
        }

        generator = templates.get(incident_type)
        if generator:
            return generator(service)
        return "No runbook template available"

    def _high_error_rate_runbook(self, service: str) -> str:
        return f"""
# High Error Rate Runbook: {service}

## Alert Condition
Error rate exceeds 1% of requests over 5 minutes.

## Immediate Actions

1. **Check recent deployments**
   ```bash
   kubectl rollout history deployment/{service} -n production
  1. Check error logs

    kubectl logs -l app={service} -n production --tail=100 | grep ERROR
    
  2. Check dependencies

    • Database connectivity
    • External API availability
    • Cache availability

Investigation Steps

  1. Identify error patterns

    Query: errors | where service == "{service}" | summarize count() by error_type
    
  2. Check recent changes

    • Code deployments
    • Configuration changes
    • Infrastructure changes
  3. Review metrics

    • Request latency
    • Resource utilization
    • Dependency health

Remediation

If caused by recent deployment:

kubectl rollout undo deployment/{service} -n production

If caused by dependency:

  • Implement circuit breaker
  • Scale up dependency
  • Fail over to backup

Escalation

If not resolved within 15 minutes:

  • Page on-call engineer: @oncall-{service}
  • Notify engineering lead: @lead-{service} """

## Developer Feedback Loop

```python
from dataclasses import dataclass
from datetime import datetime
from typing import List, Optional

@dataclass
class DeveloperFeedback:
    developer_id: str
    timestamp: datetime
    category: str  # 'tooling', 'process', 'documentation', 'support'
    satisfaction: int  # 1-5
    feedback: str
    suggestions: Optional[str] = None

class DevExFeedbackSystem:
    """Collect and analyze developer feedback"""

    def __init__(self, storage):
        self.storage = storage

    def submit_feedback(self, feedback: DeveloperFeedback):
        """Submit developer feedback"""
        self.storage.save(feedback)

        # Auto-create issues for low satisfaction
        if feedback.satisfaction <= 2:
            self._create_improvement_issue(feedback)

    def get_satisfaction_trends(self, days: int = 90) -> dict:
        """Get satisfaction trends over time"""
        feedbacks = self.storage.get_recent(days)

        by_category = {}
        by_week = {}

        for fb in feedbacks:
            # By category
            if fb.category not in by_category:
                by_category[fb.category] = []
            by_category[fb.category].append(fb.satisfaction)

            # By week
            week = fb.timestamp.isocalendar()[1]
            if week not in by_week:
                by_week[week] = []
            by_week[week].append(fb.satisfaction)

        return {
            'by_category': {
                cat: sum(scores) / len(scores)
                for cat, scores in by_category.items()
            },
            'by_week': {
                week: sum(scores) / len(scores)
                for week, scores in by_week.items()
            },
            'overall': sum(fb.satisfaction for fb in feedbacks) / len(feedbacks) if feedbacks else 0
        }

    def get_improvement_areas(self) -> List[dict]:
        """Identify areas needing improvement"""
        trends = self.get_satisfaction_trends()

        improvements = []
        for category, score in trends['by_category'].items():
            if score < 3.5:
                feedbacks = self.storage.get_by_category(category)
                common_issues = self._extract_common_issues(feedbacks)

                improvements.append({
                    'category': category,
                    'satisfaction_score': score,
                    'common_issues': common_issues,
                    'suggested_actions': self._suggest_actions(category, common_issues)
                })

        return sorted(improvements, key=lambda x: x['satisfaction_score'])

Key DevEx Principles

  1. Reduce Friction: Eliminate toil and repetitive tasks
  2. Fast Feedback: Quick build, test, and deploy cycles
  3. Self-Service: Enable developers to help themselves
  4. Clear Documentation: Up-to-date, searchable, contextual
  5. Listen and Iterate: Continuously gather and act on feedback

Developer experience in 2021 became a competitive advantage. Organizations that invest in DevEx attract better talent and ship faster.

Resources

Michael John Pena

Michael John Pena

Data & AI Director and Microsoft MVP based in Sydney. Writing about Microsoft Fabric, Azure, data engineering, and AI.