Nobody Hands You Day One: Stepping In as CTO on a Live Product Across Three Runtimes

Nobody Hands You Day One: Stepping In as CTO on a Live Product Across Three Runtimes

没有人会给你“第一天”:作为 CTO 接手一个跨三个运行时的线上产品

Most engineering stories start at day one with a clean repo. This one did not, and I think the messy version is the more useful one to read. I stepped in as CTO at RAQTS, a sports-tech platform that connects players and facilities, during a phase where the stakes were high and there was no room to stop and rebuild. 大多数工程故事都始于一个干净的代码仓库。但这个故事并非如此,我认为这种“混乱”的版本反而更具参考价值。我以 CTO 的身份加入了 RAQTS——一个连接球员与场馆的体育科技平台。当时正处于关键时期,我们没有停下来重构的余地。产品已经上线,真实的球员和场馆正依赖它运行。我的工作不是从零开始设计,而是在产品运行的同时保持稳定并持续交付。如果你只参与过从零开始的项目,这种区别听起来可能只是理论上的,但事实并非如此。它改变了你做的第一件事、触碰的第一个地方,以及你被允许触碰事物的顺序。以下是这份工作教会我的东西。

The first task is reading, not writing. When you join a live system, the cost of a careless change is not a failed test. It is a facility that cannot run its session tonight because you shipped a change to a subsystem you did not fully understand yet. So the first week was reading, not writing. How did the Unity layer talk to the backend? Where did the mobile apps get their data? What was the web portal actually responsible for, and where were the seams between all three fragile? I could not lead confidently until that map was in my head, because leadership on a live product is mostly about knowing what you are not allowed to break. The instinct is the same one every senior engineer eventually learns the hard way: move fast, but earn the right to move fast by understanding the ground first. On a greenfield project you earn that right by building. On a live one you earn it by reading. 第一项任务是阅读,而不是编写。当你加入一个线上系统时,粗心大意的代价不是测试失败,而是某个场馆今晚无法正常开展活动,仅仅因为你向一个尚未完全理解的子系统推送了变更。所以第一周我都在阅读。Unity 层是如何与后端通信的?移动端应用从哪里获取数据?Web 门户的职责到底是什么?这三者之间的脆弱连接点在哪里?在脑海中构建出这张地图之前,我无法自信地领导团队,因为线上产品的领导力很大程度上在于知道“什么东西绝对不能碰”。这与每位资深工程师最终通过惨痛教训学到的直觉是一样的:快速行动,但要通过先了解现状来赢得快速行动的权利。在全新项目中,你通过构建来赢得这种权利;而在线上项目中,你通过阅读来赢得它。

A critical phase also forces triage. Not everything can be fixed at once, and pretending otherwise is how you stall out completely. The question I kept asking was narrow: which few things most decide whether this platform stays trustworthy? The answer was the release pipeline and the live data. So that is where I put my weight first, and I let a lot of other imperfect things stay imperfect on purpose. 关键阶段也迫使你进行分类处理。不可能一次性修复所有问题,如果假装可以做到,只会导致彻底停滞。我不断问自己一个狭窄的问题:哪几件事最能决定这个平台是否值得信赖?答案是发布流水线和实时数据。所以我首先将精力集中在这些地方,并刻意让其他许多不完美的地方保持原样。

One product, three runtimes. The hardest thing about RAQTS technically was that it was one product living in three very different worlds at once. A player might interact with an interactive Unity experience called Lumo, check their standing on a React Native mobile app, and a facility might manage everything from a Next.js web portal. To the user, that is one system. Underneath, it is three stacks with three release cadences and three sets of assumptions. The discipline that keeps a split like this from fracturing is a rule I have carried across every cross-platform product I have shipped: share the truth, never the platform. 一个产品,三个运行时。从技术角度看,RAQTS 最难的地方在于它是一个同时存在于三个截然不同世界的产品。球员可能会通过名为 Lumo 的 Unity 交互体验进行互动,在 React Native 移动应用上查看排名,而场馆方则通过 Next.js Web 门户管理一切。对用户来说,这是一个系统;但在底层,它是三个技术栈、三种发布节奏和三套假设。防止这种分裂导致系统崩溃的准则,是我在交付每一个跨平台产品时都坚持的:共享事实,而非共享平台。

There has to be one authoritative source of what a score is, what a session is, what a leaderboard says. Each runtime is just a different window onto that same truth. The moment two clients start disagreeing about the state of the world, the product feels broken even if every individual app is technically working. That is the failure mode that matters, and no amount of per-app polish saves you from it. The Unity layer is a good example of the trap. Lumo is rich and interactive, and a rich client is exactly the kind of thing that wants to own its own state because that feels faster and smoother. But an interactive layer inside a larger product cannot be the place where a score is invented. It has to render the experience beautifully while treating the backend as the authority for anything that will show up somewhere else a second later. A compelling visual layer that quietly lies about the underlying data is worse than a plain one that is always honest. 必须有一个权威来源来定义什么是分数、什么是活动、什么是排行榜。每个运行时只是观察同一事实的不同窗口。一旦两个客户端对世界状态的认知产生分歧,即使每个应用在技术上都能运行,产品也会显得“坏了”。这才是真正重要的故障模式,无论你在单个应用上打磨得多么精美都无法挽救。Unity 层就是一个很好的陷阱案例。Lumo 丰富且具有交互性,而富客户端正是那种倾向于拥有自己状态的东西,因为这样感觉更快、更流畅。但大型产品中的交互层不能成为“发明分数”的地方。它必须在美观地呈现体验的同时,将后端视为权威,处理任何一秒钟后会出现在其他地方的数据。一个通过隐瞒底层数据来营造视觉效果的层,远不如一个虽然平庸但始终诚实的层。

Same logic for mobile versus web. The React Native apps and the Next.js portal serve different people. Players live on mobile for quick checks and their own standing. Facilities live on the portal for management and the wider view. The temptation is to make one a shrunken copy of the other, but a facility operator’s needs are not a player’s needs scaled down. They are a different job. What they share is not the UI. It is the data layer and the outcome model. A leaderboard position is the same object whether a player sees it on their phone or a facility sees it on a dashboard. 移动端与 Web 端也是同样的逻辑。React Native 应用和 Next.js 门户服务于不同的人群。球员使用移动端进行快速查看和关注个人排名;场馆方使用门户进行管理和查看全局。人们很容易倾向于将其中一个做成另一个的缩减版,但场馆运营者的需求并不是球员需求的简单缩小,它们是完全不同的工作。它们共享的不是 UI,而是数据层和结果模型。无论球员在手机上看到,还是场馆方在仪表盘上看到,排行榜的位置都是同一个对象。

A live leaderboard is a distributed systems problem. The feature that sounds trivial and is not is the live leaderboard. A leaderboard is easy if it is allowed to be a few minutes stale. It becomes a real engineering problem the moment it has to be live, because now every client has to converge on the same ordering, fast, while scores are still changing underneath them. The hard parts are the ones that never show up in a demo: Everyone has to agree. A player on their phone and a facility on the portal must see the same standings within moments of each other. A leaderboard that shows two different orders on two screens destroys trust instantly, because the entire point of a leaderboard is that it is the shared truth. This is a convergence problem, not a rendering one. It has to survive a bad connection. Mobile networks drop. A client that misses an update cannot be left showing a frozen, wrong board forever. It has to reconcile when it reconnects and catch up cleanly. Design for being interrupted, the same way you would design any client that cannot assume a stable link. 实时排行榜是一个分布式系统问题。这个听起来简单但实际不然的功能就是实时排行榜。如果允许排行榜有几分钟的延迟,那很简单。但一旦要求实时,它就变成了一个真正的工程难题,因为现在每个客户端都必须在分数不断变化的同时,快速收敛到相同的排序。最难的部分往往不会出现在演示中:每个人必须达成一致。手机上的球员和门户上的场馆方必须在几秒钟内看到相同的排名。如果排行榜在两个屏幕上显示出不同的顺序,会瞬间摧毁信任,因为排行榜的核心意义就在于它是共享的事实。这是一个收敛问题,而不是渲染问题。它必须在网络状况不佳时依然有效。移动网络会中断,错过更新的客户端不能一直显示冻结的错误榜单。它必须在重新连接时进行协调并干净地追赶进度。要为“中断”而设计,就像你设计任何无法假设连接稳定的客户端一样。

The write path is the truth, the read path is fast. Scores get written in one place with authority and read in many places quickly. Keeping that separation clean is what lets the board be both correct and responsive instead of trading one for the other. If you have reached for a CQRS-shaped split before, this is the same instinct wearing a jersey. The rule I held was simple and non-negotiable: the backend owns the ordering, always. No client ever computes the leaderboard from its own partial view and shows that as fact. Clients render what the authority tells them and reconcile toward it. That is slower to build than letting each app do its own math, but… 写入路径是事实,读取路径是速度。分数在一个权威的地方写入,并在多个地方快速读取。保持这种分离的清晰,才能让排行榜既准确又响应迅速,而不是二选一。如果你之前接触过 CQRS(命令查询职责分离)架构,这其实就是同样的直觉换了件马甲。我坚持的规则简单且不可商量:后端始终拥有排序的权威。没有任何客户端可以通过自己的局部视图计算排行榜并将其作为事实显示。客户端只渲染权威告知的内容,并向其靠拢。这比让每个应用自己计算要慢,但是……