More
Go back

Professional experience · ArkusNexus · 2022

Playboy Group

Repairing legacy data and maintaining the systems people relied on.

Through ArkusNexus, I worked across data migration, an internal employee platform and database maintenance for Playboy Group. My responsibilities ranged from building a Node.js repair pipeline to developing C#/.NET APIs, maintaining React interfaces and correcting data in Microsoft SQL Server.

  • Node.js
  • PostgreSQL
  • MongoDB
  • React
  • C# / .NET
  • Okta
  • SQL Server
Read the migration article
Films represented in the legacy dataset
40,000+
Measured repair processing time
160 → 20 min
Reduction in repair runtime
87.5%

01 / Context

A migration with unreliable inputs

The source was a PostgreSQL system dating back to 2006, containing information about more than 40,000 films and their contributors. The migration was described as roughly three million related records across films, people and their relationships. That figure describes the wider dataset, rather than three million individual films.

Moving to MongoDB required assembling film documents from related information: finding a film, resolving its contributors and roles, constructing the aggregate and validating the result. Missing values and inconsistent metadata could therefore interrupt the transformation or leave an incomplete document.

I worked with a senior engineer to investigate an initial migration that took approximately three days. Improving its speed exposed a separate problem: the transformation still depended on assumptions that the legacy data did not consistently satisfy. A value could be absent, fail a numeric conversion or look valid while disagreeing with a historical record.

A valid type does not establish a correct value. A plausible date can still be the wrong date.

Following a colleague’s initial suggestion and discussion with the team, I built a separate server-side Node.js process to inspect and repair the PostgreSQL dataset before migration. This made reconciliation an explicit stage, with MongoDB document construction happening after known inconsistencies had been addressed.

Repair first, transform afterwards

A conceptual view of the boundary between data reconciliation and migration. Uncertain cases leave the automated path for investigation.

Diagram preview loads as you scroll. Mermaid source is available below.

View Mermaid source
Repair first, transform afterwards · Mermaid
flowchart TD
    accTitle: Repair before migration
    accDescr: Inspect PostgreSQL data, reconcile flagged values with a historical backup, verify corrections and then migrate. Ambiguous records need investigation.
    source[(Legacy PostgreSQL)] --> inspect[Inspect a bounded batch]
    inspect --> valid{Already correct?}
    valid -->|Yes| ready[Validated data]
    valid -->|No| reconcile[Apply a repair policy]
    backup[(Historical PostgreSQL backup)] --> reconcile
    reconcile --> safe{Safe correction established?}
    safe -->|No| review[Separate for investigation]
    safe -->|Yes| verify[Revalidate the proposed correction]
    verify -->|Pass| write[Write correction to source dataset]
    verify -->|Fail| review
    write --> ready
    ready --> transform[Construct film aggregates]
    transform --> target[(MongoDB)]

Download .mmd file

02 / Implementation

Giving each data problem its own handler

I organised the repair logic using the Chain of Responsibility pattern. Each handler recognised a condition and either dealt with it or passed it onwards. The cases included already-correct values, undefined properties, NaN, null values and populated values that were incorrect.

These cases needed different policies. An optional null could be acceptable, while a missing required value needed reconciliation. A correct value required no update. A value that conflicted with historical information needed a reliable entity match and a rule establishing whether the backup value was safe to restore.

The repair decision chain

Handlers separate the failure cases. This diagram reconstructs the design; the original handler order and internal names are not retained.

Diagram preview loads as you scroll. Mermaid source is available below.

View Mermaid source
The repair decision chain · Mermaid
flowchart TD
    accTitle: Chain of Responsibility
    accDescr: Each handler either handles a condition or passes it to the next. Valid data is unchanged, flagged data is reconciled, and unmatched cases require investigation. The order is illustrative.
    input[Value and record context] --> correct{Correct?}
    correct -->|Yes| keep[Keep without writing]
    correct -->|Pass| absent{Undefined?}
    absent -->|Pass| number{NaN?}
    number -->|Pass| empty{Null?}
    empty -->|Pass| conflict{Incorrect value?}
    absent -->|Handle| policy[Apply condition-specific repair policy]
    number -->|Handle| policy
    empty -->|Handle| policy
    conflict -->|Handle| policy
    conflict -->|Pass| review[Investigate unresolved case]
    policy --> decision{Safe historical value or allowed absence?}
    decision -->|Yes| verify[Verify result before accepting]
    decision -->|No| review

Download .mmd file

The PostgreSQL backup supplied historical reference data. The process compared suspect information with its corresponding record, accepted deterministic corrections and separated unresolved cases for investigation. Replacing missing values with arbitrary defaults would have concealed uncertainty, so a repair needed more evidence than a value that merely passed a type check.

Condition-specific repair handlers · JavaScript
// Reconstructed JavaScript: scalar-value policy example.
// The field policy decides whether a historical value is safe.
const recover = (context, reason) => {
  const { reference, policy } = context;

  if (!reference.matched ||
      !policy.isValid(reference.value) ||
      !policy.canRestore(context)) {
    return { action: 'review', reason };
  }

  return { action: 'repair', value: reference.value, reason };
};

const handlers = [
  {
    matches: c => c.policy.isValid(c.value) &&
      c.policy.isVerified(c),
    handle: () => ({ action: 'keep' }),
  },
  {
    matches: c => c.value === undefined,
    handle: c => recover(c, 'undefined'),
  },
  {
    matches: c => Number.isNaN(c.value),
    handle: c => recover(c, 'invalid-number'),
  },
  {
    matches: c => c.value === null,
    handle: c => c.policy.nullable
      ? { action: 'keep' }
      : recover(c, 'required-value-missing'),
  },
  {
    matches: c => c.policy.hasConfirmedConflict(c),
    handle: c => recover(c, 'historical-conflict'),
  },
];

function classify(context) {
  const handler = handlers.find(item => item.matches(context));
  return handler
    ? handler.handle(context)
    : { action: 'review', reason: 'no-safe-rule' };
}

Here, the policy methods stand in for field-specific validation and reconciliation rules. They deliberately keep the decision about a safe historical replacement separate from recognising an undefined, null or invalid numeric value.

A correction was accepted only after checking its expected structure, agreement with the selected historical reference where applicable and compatibility with the transformation that had failed. Correct values could pass through without another write. This supported repeatable checking, although I cannot claim a crash-recovery or checkpoint mechanism from the records I retained.

03 / Optimisation

Changing the unit of work

The first repair implementation took approximately two hours and forty minutes. Repeated source reads, historical lookups and individual corrections made database interaction a substantial part of the work. Organising the handlers improved the logic; reducing those round trips improved the runtime.

Before · individual records

Read a record, inspect it, fetch its historical reference and write a correction. Repeat the database interactions for the next record.

After · bounded batches

Load a bounded set, classify it in memory, collect the necessary reference data together and write only the required corrections.

Batch reconciliation loop · JavaScript
// Reconstructed pseudocode; repository methods are illustrative.
// No original batch size, SQL statements or schema are implied.
for await (const batch of source.readBatches(batchSize)) {
  const candidates = batch.filter(needsReconciliation);
  const references = await backup.findForBatch(candidates);
  const corrections = [];

  for (const record of candidates) {
    const context = buildContext(record, references);
    const result = classify(context);

    if (result.action === 'review') {
      await review.collect(record, result.reason);
      continue;
    }

    if (result.action === 'repair') {
      const proposed = applyCorrection(record, result);

      // Check structure, historical agreement and transformation.
      if (canAcceptRepair(proposed, context)) {
        corrections.push({ before: record, after: proposed });
      } else {
        await review.collect(record, 'verification-failed');
      }
    }
  }

  if (corrections.length > 0) {
    await source.writeCorrections(corrections);
  }
}

Batching kept the working set bounded and reduced repeated database operations. Already-correct values avoided unnecessary writes, while historical reconciliation was reserved for flagged cases. The example illustrates that structure; the original batch size and query implementation are not available.

Repair runtime · equivalent operation

(160 − 20) ÷ 160 = 87.5% less processing time. These timings measure repair only. The earlier three-day migration is a separate baseline, with no verified combined end-to-end measurement.

04 / Product engineering

Maintaining the employee platform

Alongside the data work, I developed and maintained an internal platform used to manage and view employee information across Playboy Group. The application combined a React front end with C#/.NET backend services.

  • REST APIs and employee profiles. I created and maintained endpoints covering GET, POST, PUT and DELETE operations, and implemented backend functionality and data structures for the employee profile section.
  • Authentication failure handling. I implemented custom ASP.NET/.NET middleware to handle Okta authentication failures and return appropriate API responses.
  • API diagnosis and correction. I investigated incomplete responses and incorrectly structured or ordered data, then corrected the backend behaviour affecting the interface.
  • React maintenance. I resolved UI bugs and maintained integration with the backend APIs so that employee information was presented correctly.

From the interface to the employee API

A conceptual request flow showing the authentication boundary and the separate path for an unsuccessful sign-in.

Diagram preview loads as you scroll. Mermaid source is available below.

View Mermaid source
From the interface to the employee API · Mermaid
sequenceDiagram
    accTitle: Employee platform request flow
    accDescr: A conceptual request flow from React through ASP.NET authentication and profile endpoints, with a separate response path for failed authentication.
    participant UI as React interface
    participant Auth as ASP.NET / Okta authentication
    participant API as Employee profile API
    UI->>Auth: Authenticated API request
    alt Authentication fails
        Auth-->>UI: Appropriate authentication error response
    else Authentication succeeds
        Auth->>API: Continue to the requested endpoint
        API-->>UI: Employee profile response
        UI->>UI: Render the returned profile data
    end

Download .mmd file
Authentication response middleware · C#
// Reconstructed ASP.NET Core example: normalise a 401 response.
// Okta token validation remains in the authentication configuration.
public sealed class AuthenticationResponseMiddleware
{
    private readonly RequestDelegate _next;

    public AuthenticationResponseMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        await _next(context);

        if (context.Response.StatusCode == StatusCodes.Status401Unauthorized
            && !context.Response.HasStarted)
        {
            await context.Response.WriteAsJsonAsync(new
            {
                error = "authentication_required",
                message = "A valid sign-in is required."
            }, context.RequestAborted);
        }
    }
}

// Register before authentication and authorisation so that it can
// inspect their response on the way back through the pipeline.
app.UseMiddleware<AuthenticationResponseMiddleware>();
app.UseAuthentication();
app.UseAuthorization();

This reconstruction illustrates returning a predictable body for an unauthenticated request. It assumes an existing Okta authentication configuration; it does not implement token validation. Its 401 response handling leaves authorisation failures and unrelated server errors to their respective paths. For the framework’s authentication behaviour, see the ASP.NET Core documentation.

JavaScript and TypeScript were my main technologies during this period. My C#/.NET experience was practical and focused on API development, middleware, debugging and integration within an existing application.

05 / Data maintenance

Correcting the data behind the platform

My database responsibilities also included Microsoft SQL Server maintenance within the platform environment, using SQL and C#/.NET to support corrective operations. This was a separate area of work from the PostgreSQL-to-MongoDB migration.

  • Completed missing film metadata, including recording dates and information about performers, filmmakers and directors.
  • Corrected inaccurate or missing contributor information, including birth dates and other descriptive metadata.
  • Updated user statuses in response to platform support requests received by the team.
  • Used targeted SQL queries to resolve data-quality issues affecting platform behaviour.

This work connected application debugging with the underlying data: an incomplete response or an incorrect value in the interface could require a backend fix, a data correction or both. It gave me experience following issues across the React interface, .NET services and database layer.

06 / Reflection

What I took from the work

The central lesson was to separate data correctness from transformation performance. Explicit repair policies made exceptional cases understandable. Historical comparison provided evidence for specific corrections. Batching reduced the cost of applying those decisions across the dataset.

The repair process ran in the Node.js server and was initiated through Postman as a long-running HTTP request. Today, I would move that operation into a background job with progress reporting, checkpoints and retained reconciliation results. These are improvements I would make now, rather than capabilities I claim for the original implementation.

Across the engagement, I combined a focused data-repair contribution with the day-to-day engineering needed to maintain an employee platform: usable API responses, reliable authentication handling, connected interfaces and corrected records.

Based on my project notes and Migrating Data Without Trusting the Data. Diagrams are conceptual and include downloadable Mermaid source. Code examples are reconstructions, not excerpts from the client’s repository.