Work  /  Product Architecture

Product Architecture

Making a complex candidate journey explain itself

As one of two designers on the project, I helped redesign Applicant Center around a simple problem: candidates could complete individual steps, but the system failed to explain what was happening between them. I focused on making progress, validation and recovery visible across a mobile-first journey used for sensitive, high-stakes tasks.

OUTCOMES

Less uncertainty between every step

8Core information areas
5Review stages
375pxMobile-first baseline
2Designers on project
1Shared interaction model

CONSCEPT ENGAGEMENT ASSESSMENT

A clearer view of the work ahead.

A practical record of the complexity observed, the activities likely to help, and the work that was actually conducted.

Complexity profile

Discovery ComplexityPrimary driver4 / 5
Organisational Complexity3 / 5
Product and Technical ComplexityPrimary driver4 / 5
Change and Adoption Complexity3 / 5
Risk and UncertaintyPrimary driver5 / 5
Delivery ComplexityPrimary driver4 / 5
OVERALL COMPLEXITYTransformation programme

Likely engagement

Discovery workshops

Complete

Driving requirementDiscovery Complexity

A facilitated working session to align perspectives, make decisions, and agree the next action.

Stakeholder mapping

Complete

Driving requirementOrganisational Complexity

Made relationships, dependencies, and gaps visible so the team could prioritise the right intervention.

Multi-platform architecture audit

Complete

Driving requirementProduct and Technical Complexity

Examined the current experience and evidence to identify risks, duplication, and opportunities for improvement.

Change-impact assessment

Complete

Driving requirementChange and Adoption Complexity

Examined the current experience and evidence to identify risks, duplication, and opportunities for improvement.

Pilot and phased rollout

Complete

Driving requirementChange and Adoption Complexity · Risk and Uncertainty

A focused activity to reduce uncertainty, support better decisions, and move the engagement forward.

Programme plan

Complete

Driving requirementDelivery Complexity

A focused activity to reduce uncertainty, support better decisions, and move the engagement forward.

(+ 52 activities were included in this project)

The real problem was between the steps

Applicant Center supported a sensitive, multi-step candidate journey: providing personal information, completing required tasks, uploading documents, and waiting for decisions.

The individual screens were functional. The problem was that the experience did not consistently explain what was happening between them.

Candidates were left asking:

  • How much of the process is left?

  • Did my document upload successfully?

  • Why am I being asked for this information?

  • What happens after I submit?

  • Can I safely leave and return later?

These gaps created uncertainty at precisely the moments when confidence mattered most.

“The journey worked. The joins between the steps did not.”

Starting with the problems candidates actually needed solved

As one of two designers on the project, I focused on the underlying journey rather than treating each screen as an isolated UI problem.

We mapped the experience around four recurring failure points:

  1. Repeated or unnecessary information requests

  2. Progress indicators that did not reflect the real process

  3. Status changes that were invisible to candidates

  4. Weak recovery paths when something went wrong

This reframed the work from “make the interface more modern” to:

Make the system explain itself at every important handoff.

Designing for the smallest, most demanding context

We treated mobile as the primary design constraint rather than a reduced version of desktop.

This forced important questions early:

  • Can the task be completed comfortably with one hand?

  • Is the next action obvious without scanning the whole screen?

  • Can the user understand the current state at a glance?

  • Can progress be resumed after an interruption?

  • Are errors actionable rather than merely descriptive?

The mobile-first approach helped expose problems that were easy to overlook on larger screens. If a flow only worked when there was plenty of room, it was not yet simple enough.

“Mobile was not a breakpoint. It was a pressure test for the whole experience.”

tsx
function ApplicationProgress() {
  const [selectedStep, setSelectedStep] = useState(2);
  const [expandedStep, setExpandedStep] = useState(null);

  const steps = [
    {
      label: "Personal details",
      status: "complete",
      explanation: "Your personal information has been received and saved.",
      next: "Your application is ready for document review.",
    },
    {
      label: "Upload documents",
      status: "complete",
      explanation: "Your required documents have been uploaded successfully.",
      next: "The documents will now be checked.",
    },
    {
      label: "Application review",
      status: "current",
      explanation: "Our team is reviewing your application and may contact you if anything is missing.",
      next: "Review usually takes 1–3 working days.",
    },
    {
      label: "Additional information",
      status: "waiting",
      explanation: "This step will become available if the review team needs more information.",
      next: "You will receive a notification if action is needed.",
    },
    {
      label: "Decision",
      status: "error",
      explanation: "We could not complete this step because additional information is required.",
      next: "Check your messages and provide the missing information.",
    },
  ];

  const selected = steps[selectedStep];

  const statusLabel = {
    complete: "Completed",
    current: "In progress",
    waiting: "Waiting",
    error: "Action needed",
  };

  const statusColor = {
    complete: "#8FD3B6",
    current: "#FF8A66",
    waiting: "#AAB2BC",
    error: "#FF8A66",
  };

  return (
    <section
      style={{
        width: "100%",
        boxSizing: "border-box",
        padding: "clamp(24px, 5vw, 56px)",
        background: "#11161F",
        color: "#FFFFFF",
        fontFamily: "Inter, Arial, sans-serif",
        borderRadius: "12px",
      }}
    >
      <div style={{ maxWidth: "760px", margin: "0 auto" }}>
        <div style={{ marginBottom: "36px" }}>
          <div
            style={{
              marginBottom: "12px",
              color: "#FF8A66",
              fontSize: "12px",
              fontWeight: 700,
              letterSpacing: "0.14em",
            }}
          >
            APPLICATION PROGRESS
          </div>

          <h2
            style={{
              margin: "0 0 12px",
              fontSize: "clamp(28px, 5vw, 52px)",
              lineHeight: 1.05,
              letterSpacing: "-0.04em",
            }}
          >
            Your application, explained
          </h2>

          <p
            style={{
              maxWidth: "620px",
              margin: 0,
              color: "#C8CCD0",
              fontSize: "16px",
              lineHeight: 1.6,
            }}
          >
            Every step has a visible status, a clear explanation, and an
            answer to what happens next.
          </p>
        </div>

        <div
          style={{
            display: "grid",
            gap: "12px",
          }}
        >
          {steps.map((step, index) => {
            const isSelected = selectedStep === index;
            const isExpanded = expandedStep === index;
            const color = statusColor[step.status];

            return (
              <div
                key={step.label}
                style={{
                  border: isSelected
                    ? "2px solid #FF8A66"
                    : "1px solid #303844",
                  borderRadius: "10px",
                  background: isSelected ? "#202936" : "#191F29",
                  overflow: "hidden",
                }}
              >
                <button
                  type="button"
                  onClick={() => setSelectedStep(index)}
                  style={{
                    display: "flex",
                    alignItems: "center",
                    gap: "16px",
                    width: "100%",
                    padding: "18px",
                    border: 0,
                    background: "transparent",
                    color: "#FFFFFF",
                    textAlign: "left",
                    cursor: "pointer",
                  }}
                >
                  <span
                    style={{
                      display: "grid",
                      placeItems: "center",
                      width: "32px",
                      height: "32px",
                      flexShrink: 0,
                      borderRadius: "50%",
                      background: color,
                      color: "#11161F",
                      fontSize: "14px",
                      fontWeight: 800,
                    }}
                  >
                    {step.status === "complete"
                      ? "✓"
                      : step.status === "error"
                        ? "!"
                        : index + 1}
                  </span>

                  <span style={{ flex: 1 }}>
                    <strong
                      style={{
                        display: "block",
                        marginBottom: "4px",
                        fontSize: "16px",
                      }}
                    >
                      {step.label}
                    </strong>

                    <span
                      style={{
                        color,
                        fontSize: "12px",
                        fontWeight: 700,
                        letterSpacing: "0.08em",
                        textTransform: "uppercase",
                      }}
                    >
                      {statusLabel[step.status]}
                    </span>
                  </span>

                  <span
                    style={{
                      color: "#AAB2BC",
                      fontSize: "20px",
                      transform: isSelected ? "rotate(90deg)" : "none",
                    }}
                  >
                    →
                  </span>
                </button>

                {isSelected && (
                  <div
                    style={{
                      padding: "0 18px 18px 66px",
                      color: "#C8CCD0",
                      fontSize: "14px",
                      lineHeight: 1.6,
                    }}
                  >
                    <p style={{ margin: "0 0 12px" }}>
                      {step.explanation}
                    </p>

                    <button
                      type="button"
                      onClick={() =>
                        setExpandedStep(isExpanded ? null : index)
                      }
                      style={{
                        padding: 0,
                        border: 0,
                        background: "transparent",
                        color: "#FF8A66",
                        fontSize: "13px",
                        fontWeight: 700,
                        cursor: "pointer",
                      }}
                    >
                      {isExpanded ? "Hide details" : "What happens next?"}
                    </button>

                    {isExpanded && (
                      <div
                        style={{
                          marginTop: "12px",
                          padding: "14px",
                          borderLeft: "3px solid #FF8A66",
                          background: "#11161F",
                          color: "#FFFFFF",
                        }}
                      >
                        {step.next}
                      </div>
                    )}
                  </div>
                )}
              </div>
            );
          })}
        </div>

        <div
          style={{
            display: "flex",
            alignItems: "flex-start",
            gap: "12px",
            marginTop: "28px",
            padding: "18px",
            borderRadius: "10px",
            background: "#FF8A66",
            color: "#11161F",
          }}
        >
          <span style={{ fontSize: "18px", fontWeight: 800 }}>→</span>
          <div>
            <strong style={{ display: "block", marginBottom: "4px" }}>
              Next step: {selected.label}
            </strong>
            <span style={{ fontSize: "14px", lineHeight: 1.5 }}>
              {selected.next}
            </span>
          </div>
        </div>
      </div>
    </section>
  );
}

render(<ApplicationProgress />);

Turning invisible system states into visible feedback

A major part of the work was making the system’s internal state legible to candidates.

Instead of showing only a completed form or a generic confirmation, each important action received a clear response:

  • Information received

  • Document uploaded

  • Verification in progress

  • Action required

  • Submission complete

  • Waiting for the next stage

The experience became easier to understand because each transition had a visible receipt.

“No important action should end in silence.”

Reducing friction without reducing reassurance

The solution was not simply to remove steps. Some information was necessary because of the operational and compliance requirements behind the experience.

The goal was to make the reason, timing, and consequence of each request clearer.

Where information already existed, the experience could confirm it rather than ask the candidate to enter it again. Where a task could be deferred, it was separated from the immediate flow. Where a delay was unavoidable, the interface explained what was happening.

This allowed the experience to remain thorough without feeling unnecessarily difficult.

Creating a system that could scale beyond one journey

The redesign introduced patterns that could be reused across the broader Applicant Center experience:

  • Consistent progress communication

  • Shared validation behaviour

  • Repeatable upload states

  • Clear recovery patterns

  • Mobile-first form structures

  • Standardised confirmation and waiting states

The value was not only in improving one sequence. It was in creating a more coherent language for future journeys.

tsx
function CandidateTaskExplorer() {
  const [state, setState] = useState("default");
  const [mobile, setMobile] = useState(false);
  const [value, setValue] = useState("");

  const isError = state === "error";
  const isSuccess = state === "success";
  const isDisabled = state === "disabled";
  const isFocused = state === "focus";

  const orange = "#FF8A66";
  const dark = "#11161F";
  const surface = "#1F2633";
  const muted = "#C8CCD0";
  const border = "#3B4552";

  const borderColor = isError
    ? orange
    : isFocused
      ? orange
      : isSuccess
        ? "#8FD3B6"
        : border;

  return (
    <section
      style={{
        width: "100%",
        boxSizing: "border-box",
        padding: "clamp(24px, 5vw, 56px)",
        background: dark,
        color: "#FFFFFF",
        fontFamily: "Inter, Arial, sans-serif",
        borderRadius: "12px",
      }}
    >
      <div
        style={{
          maxWidth: "980px",
          margin: "0 auto",
        }}
      >
        <div style={{ marginBottom: "32px" }}>
          <div
            style={{
              marginBottom: "12px",
              color: orange,
              fontSize: "12px",
              fontWeight: 700,
              letterSpacing: "0.14em",
            }}
          >
            DESIGN SYSTEM EXPLORER
          </div>

          <h2
            style={{
              maxWidth: "680px",
              margin: "0 0 12px",
              fontSize: "clamp(28px, 5vw, 52px)",
              lineHeight: 1.05,
              letterSpacing: "-0.04em",
            }}
          >
            One task, five useful states
          </h2>

          <p
            style={{
              maxWidth: "680px",
              margin: 0,
              color: muted,
              fontSize: "16px",
              lineHeight: 1.6,
            }}
          >
            The same component adapts to the user’s context without changing
            its underlying behaviour.
          </p>
        </div>

        <div
          style={{
            display: "flex",
            flexWrap: "wrap",
            gap: "8px",
            marginBottom: "28px",
          }}
        >
          {["default", "focus", "error", "success", "disabled"].map(
            (option) => (
              <button
                key={option}
                type="button"
                onClick={() => setState(option)}
                style={{
                  padding: "10px 14px",
                  border: state === option
                    ? `2px solid ${orange}`
                    : `1px solid ${border}`,
                  borderRadius: "999px",
                  background: state === option ? orange : surface,
                  color: state === option ? dark : "#FFFFFF",
                  fontSize: "13px",
                  fontWeight: 700,
                  textTransform: "capitalize",
                  cursor: "pointer",
                }}
              >
                {option}
              </button>
            )
          )}

          <button
            type="button"
            onClick={() => setMobile(!mobile)}
            style={{
              marginLeft: "auto",
              padding: "10px 14px",
              border: `1px solid ${border}`,
              borderRadius: "999px",
              background: mobile ? orange : surface,
              color: mobile ? dark : "#FFFFFF",
              fontSize: "13px",
              fontWeight: 700,
              cursor: "pointer",
            }}
          >
            {mobile ? "Mobile layout" : "Desktop layout"}
          </button>
        </div>

        <div
          style={{
            display: "flex",
            flexDirection: mobile ? "column" : "row",
            alignItems: "stretch",
            gap: "24px",
          }}
        >
          <div
            style={{
              flex: 1,
              maxWidth: mobile ? "390px" : "none",
              padding: "24px",
              border: `1px solid ${border}`,
              borderRadius: "12px",
              background: surface,
            }}
          >
            <div
              style={{
                display: "flex",
                justifyContent: "space-between",
                gap: "16px",
                marginBottom: "24px",
              }}
            >
              <div>
                <div
                  style={{
                    marginBottom: "8px",
                    color: orange,
                    fontSize: "11px",
                    fontWeight: 700,
                    letterSpacing: "0.12em",
                  }}
                >
                  CURRENT TASK
                </div>

                <h3
                  style={{
                    margin: 0,
                    fontSize: "24px",
                    lineHeight: 1.15,
                  }}
                >
                  Upload your CV
                </h3>
              </div>

              <span
                style={{
                  display: "grid",
                  placeItems: "center",
                  width: "32px",
                  height: "32px",
                  flexShrink: 0,
                  borderRadius: "50%",
                  background: isSuccess ? "#8FD3B6" : orange,
                  color: dark,
                  fontWeight: 800,
                }}
              >
                {isSuccess ? "✓" : "2"}
              </span>
            </div>

            <label
              style={{
                display: "block",
                marginBottom: "8px",
                color: "#FFFFFF",
                fontSize: "14px",
                fontWeight: 700,
              }}
            >
              CV or résumé
            </label>

            <div
              style={{
                display: "flex",
                alignItems: "center",
                gap: "12px",
                padding: "14px",
                border: `2px solid ${borderColor}`,
                borderRadius: "8px",
                background: dark,
                boxShadow: isFocused
                  ? `0 0 0 4px rgba(255, 138, 102, 0.18)`
                  : "none",
                opacity: isDisabled ? 0.5 : 1,
              }}
            >
              <span
                style={{
                  display: "grid",
                  placeItems: "center",
                  width: "36px",
                  height: "36px",
                  flexShrink: 0,
                  borderRadius: "6px",
                  background: "rgba(255, 138, 102, 0.16)",
                  color: orange,
                  fontSize: "18px",
                }}
              >
                ↑
              </span>

              <input
                value={isSuccess ? "andreistanescu-cv.pdf" : value}
                disabled={isDisabled || isSuccess}
                onChange={(event) => setValue(event.target.value)}
                placeholder="Choose a file"
                aria-invalid={isError}
                aria-describedby="task-help task-error"
                style={{
                  minWidth: 0,
                  flex: 1,
                  border: 0,
                  outline: 0,
                  background: "transparent",
                  color: "#FFFFFF",
                  fontSize: "14px",
                }}
              />
            </div>

            <p
              id="task-help"
              style={{
                margin: "10px 0 0",
                color: muted,
                fontSize: "13px",
                lineHeight: 1.5,
              }}
            >
              PDF, DOC, or DOCX. Maximum file size: 10MB.
            </p>

            {isError && (
              <p
                id="task-error"
                style={{
                  margin: "10px 0 0",
                  color: orange,
                  fontSize: "13px",
                  fontWeight: 700,
                }}
              >
                Please upload a supported file before continuing.
              </p>
            )}

            {isSuccess && (
              <p
                style={{
                  margin: "10px 0 0",
                  color: "#8FD3B6",
                  fontSize: "13px",
                  fontWeight: 700,
                }}
              >
                Upload complete. Your document is ready for verification.
              </p>
            )}

            <button
              type="button"
              disabled={isDisabled}
              style={{
                width: "100%",
                minHeight: "48px",
                marginTop: "24px",
                border: 0,
                borderRadius: "8px",
                background: isDisabled ? "#56606D" : orange,
                color: dark,
                fontSize: "14px",
                fontWeight: 800,
                cursor: isDisabled ? "not-allowed" : "pointer",
              }}
            >
              {isSuccess ? "Uploaded" : "Continue"}
            </button>
          </div>

          <aside
            style={{
              flex: "0 1 320px",
              padding: "24px",
              borderLeft: `3px solid ${orange}`,
              borderRadius: "8px",
              background: "#191F29",
            }}
          >
            <div
              style={{
                marginBottom: "14px",
                color: orange,
                fontSize: "11px",
                fontWeight: 700,
                letterSpacing: "0.12em",
              }}
            >
              ACCESSIBILITY GUIDANCE
            </div>

            <ul
              style={{
                display: "grid",
                gap: "14px",
                margin: 0,
                paddingLeft: "18px",
                color: muted,
                fontSize: "14px",
                lineHeight: 1.55,
              }}
            >
              <li>Use a visible focus ring with sufficient contrast.</li>
              <li>Expose errors next to the field and through assistive technology.</li>
              <li>Keep the control usable with a keyboard.</li>
              <li>Use a minimum 48px touch target on mobile.</li>
              <li>Never communicate state through colour alone.</li>
            </ul>
          </aside>
        </div>
      </div>
    </section>
  );
}

render(<CandidateTaskExplorer />);

The outcome was more than a visual redesign

Applicant Center became easier to understand because the experience began to communicate the work happening behind the interface.

The redesign helped make progress visible, reduced ambiguity around submissions and handoffs, and gave the team a stronger foundation for handling complex candidate journeys consistently.

The most important shift was conceptual:

We stopped designing isolated screens and started designing the candidate’s understanding of the process.