<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged webarchitecture</title>
<link>https://coderlegion.com/tag/webarchitecture</link>
<description>Powered by Question2Answer</description>
<item>
<title>The Engineering Behind ShiftSync: Turning Workforce Rostering Into a Constraint-Optimisation Problem</title>
<link>https://coderlegion.com/26044/engineering-behind-shiftsync-turning-workforce-rostering-constraint-optimisation-problem</link>
<description>&lt;p&gt;&lt;em&gt;How 20+ scheduling algorithms, a privacy-first local architecture, and zero mandatory registration combine to make free AI-powered shift scheduling actually work&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Workforce scheduling looks like a UI problem until you try to automate it correctly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Then the real engineering begins.&lt;/strong&gt;&lt;br&gt;
A calendar is trivial to draw. An accurate, fair, legally compliant roster for a 60-person hospital ward covering a 28-day planning period — that is a non-trivial constraint-satisfaction problem that has been forcing managers to stay late with spreadsheets for decades.&lt;br&gt;
ShiftSync is my attempt to solve this properly. It is a browser-based, offline-capable, privacy-first workforce scheduling application with 20+ mathematical algorithms, full leave management, fatigue enforcement, and PDF/Excel export — free, no registration required.&lt;br&gt;
Source code: github.com/validivar/ShiftSync Live application: www.shiftsync.world&lt;br&gt;
This article is about the engineering decisions that made it work — and the ones I had to revisit when my first approach turned out to be wrong.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Start With Constraints, Not the Interface&lt;/strong&gt;&lt;br&gt;
The most seductive mistake in scheduling software is starting with the UI.&lt;br&gt;
You build the staff table. You build the weekly grid. You add colour-coded shift indicators. It looks impressive. Then someone asks: how does it automatically generate a valid schedule?&lt;br&gt;
That is when you discover you have been building the wrong thing first.&lt;br&gt;
Before any interface code, a scheduling system requires a precise constraint model. ShiftSync's constraint model distinguishes between two fundamentally different types of rules:&lt;br&gt;
Hard Constraints — Inviolable&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;An employee on approved leave cannot be assigned to any shift &lt;br&gt;
during their leave period.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No employee may exceed their defined maximum consecutive shift count.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A minimum rest interval (default: 11 hours, EU Working Time &lt;br&gt;
Directive aligned) must separate the end of one shift &lt;br&gt;
and the start of the next.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Any shift designated as requiring a specific skill (ICU, ACLS, &lt;br&gt;
Forklift, etc.) must receive an employee who holds that &lt;br&gt;
certification.&lt;br&gt;
Soft Constraints — Optimised, Not Mandated&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Employees' stated shift preferences should be honoured when &lt;br&gt;
coverage allows.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Total working hours across the planning period should be &lt;br&gt;
distributed equitably across the team.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Night shift assignments should rotate across the full team &lt;br&gt;
rather than concentrating on a subset.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Individual weekly hours should approach (not necessarily &lt;br&gt;
reach) the configured minimum.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The distinction is load-bearing. An algorithm that treats all constraints identically either refuses to generate a roster (too many collisions between hard rules) or generates one that is technically valid but practically awful (maximises compliance metrics while ignoring everything a human manager actually cares about).&lt;br&gt;
The correct model is validity plus optimisation — not just assignment.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why 20 Algorithms Instead of One&lt;/strong&gt;&lt;br&gt;
My first instinct was to build a single generalised scheduling algorithm that could handle any pattern.&lt;br&gt;
This was wrong.&lt;br&gt;
Different industries use shift patterns with fundamentally incompatible structures. The Panama Plan's 2-2-3 cycle is not a generalisation of the Dupont rotation's 28-day sequence. A 24-hour call duty schedule has completely different rest constraint arithmetic than a standard 4x4 eight-hour pattern.&lt;br&gt;
Trying to represent all of these inside a single algorithm produces either a combinatorial explosion of special cases or a lowest-common-denominator system that handles none of them well.&lt;br&gt;
The correct approach is a dispatch table — separate, cleanly implemented algorithms for each pattern, each with full knowledge of its own cycle structure and timing requirements.&lt;br&gt;
const algorithms = {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;'4x4':   generate4x4Schedule,    // 8hr 3-shift rotation
'panama': generatePanamaSchedule, // 12hr 2-2-3 cycle
'pitman': generatePitmanSchedule, // 12hr 4-on-4-off
'dupont': generateDupontSchedule, // 12hr 28-day complex rotation
'ddnnoo': generateDDNNOOSchedule, // 12hr D-D-N-N-O-O cycle
'4-10':  generate4_10Schedule,   // 10hr 4-day week
'8hour': generate8HourSchedule,  // 8hr standard 5/2
'12hour':generate12HourSchedule, // 12hr 3-on-3-off
'24hour':generate24HourSchedule, // 24hr call duty
'48hour':generate48HourSchedule, // 48hr weekend call
'mixed': generateMixedSchedule   // hybrid 8hr+12hr extended nights
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;};&lt;/p&gt;
&lt;p&gt;const fn = algorithms[config.shiftType] || generateFairSchedule;&lt;br&gt;
return fn(startDate, period, staffMembers.length, config);&lt;br&gt;
Each function receives: start date (Luxon DateTime), planning period in days, staff count, and the full config object. Each independently implements its cycle logic before calling checkStaffLeave on every staff-date combination to enforce leave constraints.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Deep Dive: The Dupont Algorithm&lt;/strong&gt;&lt;br&gt;
The Dupont is the most complex pattern to implement correctly because its cycle is 28 days long and mixes night shifts, day shifts, and rest in a non-obvious sequence.&lt;br&gt;
The canonical Dupont sequence is:&lt;br&gt;
N N N N O O O D D D O N N N O O O D D D D O O O O O O O&lt;br&gt;
Where N = Night, D = Day, O = Off.&lt;br&gt;
The critical implementation challenge is assigning teams to positions in this cycle such that:&lt;br&gt;
1.All shifts are covered on all days&lt;br&gt;
2.No team is assigned the same position in consecutive cycles&lt;br&gt;
3.Employee preferences shift the assignment within the cycle without breaking coverage&lt;br&gt;
function generateDupontSchedule(startDate, days, staffCount, config) {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const schedule = [];
const dp = [
    'N','N','N','N','O','O','O',  // 4 nights, 3 off
    'M','M','M','O',               // 3 days, 1 off
    'N','N','N','O','O','O',       // 3 nights, 3 off
    'M','M','M','M',               // 4 days
    'O','O','O','O','O','O','O'   // 7 off
];

for (let day = 0; day &amp;lt; days; day++) {
    const currentDate = startDate.plus({ days: day });
    const dayEntry = { 
        date: currentDate.toFormat('yyyy-MM-dd'),
        dayOfWeek: currentDate.toFormat('EEE'),
        formattedDate: currentDate.toFormat('MMM dd'),
        shifts: {}
    };
    
    staffMembers.forEach((staff, i) =&amp;gt; {
        // Offset each staff member's position in the cycle
        // to ensure continuous coverage
        const cyclePos = (day + Math.floor(i * dp.length / staffCount)) 
                         % dp.length;
        let shift = dp[cyclePos];
        
        // Hard constraint: leave always wins
        const leave = checkStaffLeave(staff, currentDate);
        if (leave) shift = 'L-' + leave;
        
        dayEntry.shifts[staff.id] = { 
            type: shift, 
            staffName: staff.fullName, 
            staffRank: staff.rank, 
            hours: shift === 'O' ? 0 : 12 
        };
    });
    
    schedule.push(dayEntry);
}
return schedule;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;br&gt;
The Math.floor(i * dp.length / staffCount) offset distributes team members across the cycle such that all 28 cycle positions have coverage on every day of the planning period.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Objective Function&lt;/strong&gt;&lt;br&gt;
Two schedules can both satisfy every hard constraint. Which one is better?&lt;br&gt;
ShiftSync's compliance scoring function evaluates quality across four dimensions:&lt;br&gt;
function calculateRosterStats(schedule) {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const stats = {
    totalHours: 0,
    shiftCount: { M:0, A:0, N:0, O:0, C:0 },
    staffHours: {},
    minHours: Infinity,
    maxHours: 0,
    avgHours: 0,
    compliant: true
};

// Accumulate per-staff hours
schedule.forEach(day =&amp;gt; {
    staffMembers.forEach(staff =&amp;gt; {
        const s = day.shifts[staff.id];
        if (s) {
            const t = s.type.charAt(0);
            if (stats.shiftCount[t] !== undefined) stats.shiftCount[t]++;
            stats.totalHours += s.hours || 0;
            stats.staffHours[staff.id] += s.hours || 0;
        }
    });
});

// Evaluate compliance bounds
const arr = Object.values(stats.staffHours);
stats.minHours = Math.min(...arr);
stats.maxHours = Math.max(...arr);
stats.avgHours = arr.reduce((a,b) =&amp;gt; a+b, 0) / arr.length;

const wks = schedule.length / 7;
const maxW = parseInt(document.getElementById('maxHoursWeek').value) || 48;
const minW = parseInt(document.getElementById('minHoursWeek').value) || 36;

stats.compliant = stats.maxHours &amp;lt;= maxW * wks 
               &amp;amp;&amp;amp; stats.minHours &amp;gt;= minW * wks * 0.5;

return stats;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;br&gt;
The compliance flag (PASS / REVIEW) surfaces in the roster display panel. It is not a gate — the manager receives the roster regardless — but the flag ensures that borderline cases are visible rather than buried.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Local-First Architecture: A Deliberate Privacy Stance&lt;/strong&gt;&lt;br&gt;
Workforce scheduling data is operationally sensitive. It reveals staffing levels, employee identities, capability distributions, and leave patterns — information that many organisations in healthcare, security, and government are legitimately concerned about transmitting to third-party cloud infrastructure.&lt;br&gt;
ShiftSync's response to this is architectural, not policy-based. The application has no server-side component. There is no API endpoint receiving scheduling data. There is no cloud database. There is no authentication layer that could become a breach surface.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Traditional SaaS&lt;/strong&gt;:  Browser → API → Database → Application → Browser&lt;br&gt;
ShiftSync:         Browser ← localStorage → Browser&lt;br&gt;
All data is stored in localStorage. All scheduling computation happens in the browser. All export (PDF, Excel) is generated client-side by jsPDF and SheetJS respectively. The application works entirely offline after the initial HTML page load.&lt;br&gt;
This architecture has real limitations for large enterprise deployments that require multi-user collaboration, centralised audit logs, and cross-device synchronisation. Those are genuine trade-offs, not ignored problems.&lt;br&gt;
But for the target audience — small to medium organisations, NGOs, community hospitals, SMEs — this architecture is not a limitation. It is an advantage. It eliminates an entire category of data management risk.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Fatigue Management System&lt;/strong&gt;&lt;br&gt;
EU Working Time Directive requires a minimum 11-hour rest period between shifts. Many clinical governance frameworks require maximum consecutive shift limits. ShiftSync enforces both.&lt;br&gt;
The consecutive shift counter runs during algorithm execution. After generation, the conflict detection system evaluates whether the staff count is sufficient to satisfy the configured limits:&lt;br&gt;
function checkConflicts() {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const maxC = parseInt(document.getElementById('maxConsecutive').value) || 5;
const alertEl = document.getElementById('conflictAlert');

if (staffMembers.length &amp;gt; 0 &amp;amp;&amp;amp; staffMembers.length &amp;lt; maxC) {
    alertEl.classList.add('show');
    document.getElementById('conflictMsg').textContent = 
        `Warning: Only ${staffMembers.length} staff for patterns 
         requiring ${maxC}+ members. Some may exceed consecutive 
         shift limits.`;
} else {
    alertEl.classList.remove('show');
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;br&gt;
The warning surfaces before generation, giving the manager the option to add staff or adjust the consecutive limit before committing to a roster that will need manual correction.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Stack Decisions and Why&lt;/strong&gt;&lt;br&gt;
&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=15378312219060673527&quot; alt=&quot;&quot;&gt;&lt;br&gt;
The absence of a framework is a deliberate choice. A scheduling tool used by a ward manager in Lagos, a factory supervisor in Manila, and an NGO coordinator in Nairobi needs to load fast on 3G and work when the connection drops. A React bundle adds seconds to the load time and introduces a dependency on JavaScript delivery infrastructure.&lt;br&gt;
Single-file HTML with CDN dependencies loads in under two seconds on a mobile connection. It works offline. It deploys by dragging a file into a hosting panel.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What the Open Source Invitation Is Really For&lt;/strong&gt;&lt;br&gt;
ShiftSync is on GitHub not primarily as a portfolio item, but because scheduling logic is domain-specific in ways that a solo developer building from first principles will get wrong.&lt;br&gt;
The DDNNOO algorithm is straightforward. But is the implementation correct for the specific variant used in ICU environments where one of the two &quot;day&quot; slots has different skill requirements from the other? I do not know. A developer who has worked scheduling in that environment might.&lt;br&gt;
The Panama Plan cycle is mathematically clear. Does it interact correctly with a 14-day planning period where the cycle straddles the boundary? The tests should tell us. They might not cover all cases.&lt;br&gt;
Open source is the mechanism for closing those gaps. If you work in scheduling, healthcare technology, workforce management, or HR software — the codebase is readable, the algorithms are individually separated, and issues are open.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Build It. Try It. Improve It.&lt;/strong&gt;&lt;br&gt;
ShiftSync is live at www.shiftsync.world.&lt;br&gt;
No signup. No email. No credit card. Open the page. Add staff. Select a pattern. Generate. Export.&lt;br&gt;
If you find a constraint the algorithm mishandles, open an issue: github.com/validivar/ShiftSync&lt;br&gt;
If you use it in a production scheduling environment and find something wrong, tell me. That feedback is the most valuable contribution the project can receive.&lt;br&gt;
And if you know an organisation — a clinic, an NGO, a small manufacturer, a security firm — still building rosters in Excel at midnight, send them the link. The better schedule is the product. Help us build it. &lt;br&gt;
The immediate goal to sustaining ShiftSync is maintaining its website. You could help towards website renewal subscription using the donation buttons on the live site.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;🌐 Live app: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.shiftsync.world&quot;&gt;www.shiftsync.world&lt;/a&gt; | ⭐ GitHub: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/validivar/ShiftSync&quot;&gt;github.com/validivar/ShiftSync&lt;/a&gt; | &lt;br&gt;
Mikhail Ikpoma — Founder, ShiftSync &lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/26044/engineering-behind-shiftsync-turning-workforce-rostering-constraint-optimisation-problem</guid>
<pubDate>Wed, 02 Sep 2026 01:40:09 +0000</pubDate>
</item>
</channel>
</rss>