
Tuesday, November 5, I was reflecting on the rebuild.
Over the long weekend, I’d scrapped the three-tier architecture and rebuilt with a flat coordinator and specialized workers. During that rebuild, I’d made several architectural decisions.
Today I’m sharing one of them: worker jobs.
The Question
As I designed workers, a question kept coming up: Should workers have exactly one job, or can they have multiple related jobs?
The PHPUnit Example
I needed a PHPUnit worker. PHPUnit has two primary modes:
- Test mode: Run tests, report pass/fail
- Coverage mode: Run tests, analyze code coverage, generate reports
Same tool. Different outputs. Different parsing logic.
The Single Responsibility Principle says one worker, one job:
- PHPUnit Test Worker
- PHPUnit Coverage Worker
But look at what those two workers would need. Both invoke the PHPUnit skill with a mode parameter. Both parse output from the skill. Both interpret results and report back.
The context is 90% identical. Only the parsing logic differs – one parses test results, the other parses coverage reports.
Creating two separate workers would violate DRY (Don’t Repeat Yourself) in a massive way.
The Conflict
Two engineering principles in direct conflict:
Single Responsibility Principle: One worker, one job
Don’t Repeat Yourself: Don’t duplicate context across workers
I couldn’t satisfy both. Separate workers violate DRY. Combined worker violates SRP.
Which principle matters more?
Reframing the Question
Then I asked myself: What is the “responsibility” we’re talking about?
Is it the worker file itself? Or is it each invocation of the worker?
When the coordinator invokes the PHPUnit worker, it specifies which job:
Invoke PHPUnit worker with job="test" Invoke PHPUnit worker with job="coverage"
Each invocation has exactly one responsibility. The coordinator never invokes both jobs simultaneously. Each call does one thing.
The worker file can contain multiple jobs, but each execution has a single responsibility.
The Resolution
SRP applies to invocations, not file structure.
When I invoke a worker, it should do one thing. But the worker file can define multiple jobs as long as they’re extremely tightly coupled.
This preserves both principles:
- SRP: Each invocation has single responsibility
- DRY: Shared context lives in one place
The key constraint: jobs must be VERY tightly coupled. Same tool, different modes. Massive shared context.
The Pattern
Guidelines I established:
- Workers can have 1-2 jobs (rarely 3)
- Jobs must be extremely tightly coupled
- Each invocation specifies which job
- Shared context stays DRY
This pattern prevented worker proliferation. Without it, I’d have ended up with 15+ workers with massive context duplication. With it, 5 workers with clear boundaries and minimal duplication.
Much easier to maintain.
Patterns Worth Sharing
Tuesday, November 5, I was documenting the patterns from the rebuild.
Worker jobs was one concept. Multiple related jobs per worker file, but single responsibility per invocation. It made sense. It solved the duplication problem.
But as I wrote, a bigger question emerged: How do I teach these concepts to other developers?
I could document the patterns. List the guidelines. Explain the decisions.
But patterns without context are just rules. Rules don’t teach thinking. And I needed to teach the thinking that led to the patterns.
That would take the rest of the week to figure out.