<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Suresh Thotakura</title>
    <description>The latest articles on DEV Community by Suresh Thotakura (@thotakura).</description>
    <link>https://dev.to/thotakura</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F373840%2F32c9e5b9-3c69-4141-9c36-7aeedce1ebd7.jpeg</url>
      <title>DEV Community: Suresh Thotakura</title>
      <link>https://dev.to/thotakura</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/thotakura"/>
    <language>en</language>
    <item>
      <title>Your Subscriber Passed the POC. Production Has a Different Test.</title>
      <dc:creator>Suresh Thotakura</dc:creator>
      <pubDate>Sat, 05 Sep 2026 18:32:29 +0000</pubDate>
      <link>https://dev.to/thotakura/your-subscriber-passed-the-poc-production-has-a-different-test-4bmc</link>
      <guid>https://dev.to/thotakura/your-subscriber-passed-the-poc-production-has-a-different-test-4bmc</guid>
      <description>&lt;p&gt;&lt;strong&gt;Your subscriber passed the POC. Production has a different test: &lt;em&gt;volume&lt;/em&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A message subscriber that does its work inline on the delivery callback is capped at one message per handler-time. At POC volume you never hit that ceiling. At production volume it is the only thing you hit. Same broker, same handler, same 1000 queued messages: 636 seconds to drain inline, 44 seconds off the callback.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why the POC never shows it&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The pattern is easy to write and easy to sign off on. The broker calls you with a message. You deserialize it, do the work, ack it, return. It is correct, it is simple, and at a few messages a minute it is indistinguishable from any other design. That is exactly why it survives review: the POC never disagrees with you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Common mistake:&lt;/strong&gt; "We load tested it and it kept up." A load test at POC volume proves the handler is fast enough for the callback to keep pace, not that the design scales. Below the ceiling, inline and hand-off look identical. The test that matters queues more messages than one handler-time per second can clear, and then measures how long the backlog takes to drain.&lt;/p&gt;

&lt;p&gt;The math is what the POC hides. A Solace context drives delivery for every flow bound to it from one thread, and the next message is not handed to you until your callback returns. So throughput is capped at 1 divided by your average handler time. Not roughly. Exactly. A bigger box does not move it. A bigger backlog does not move it, it just makes the wait longer. Solace's own docs say not to block that thread, but a warning without a number reads as theory.&lt;/p&gt;

&lt;p&gt;So I built the number.&lt;/p&gt;

&lt;p&gt;The two callbacks&lt;br&gt;
Both of these are the real OnMessageReceived from messaging-lab, a small ports-and-adapters layer over the native Solace .NET SDK with two subscriber implementations that share everything except this method. The first is shown without its try/catch and logger; the second is verbatim.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// SolaceSequentialSubscriber&amp;lt;T&amp;gt;: work inline on the delivery callback&lt;/span&gt;
&lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnMessageReceived&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MessageEventArgs&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UTF8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BinaryAttachment&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_deserializer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Deserialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_flow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Ack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ADMessageId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deserialize, handle, ack, all on the broker's single delivery thread. The next message waits for this one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// SolaceConcurrentSubscriber&amp;lt;T&amp;gt;: hand off and return&lt;/span&gt;
&lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnMessageReceived&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MessageEventArgs&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="n"&gt;_ingress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;AsTask&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;GetAwaiter&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;GetResult&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The callback only writes the message to a bounded Channel and returns. The channel is sized to the flow's WindowSize, so if the workers fall behind, the write blocks the callback and the broker's own window flow control takes over. Nothing is buffered without bound.&lt;/p&gt;

&lt;p&gt;Behind the channel, a single router task reads messages in delivery order, deserializes each one, and hashes an ordering key into one of N lanes:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;var lane = _lanes![unchecked((uint)_keySelector!.GetKey(payload).GetHashCode()) % (uint)_lanes.Length];&lt;br&gt;
await lane.Writer.WriteAsync((message, payload));&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Each lane is a single-reader channel drained by exactly one worker, which calls the handler and acks on success. Same key, same lane, same order. Different keys, different lanes, in parallel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The numbers&lt;/strong&gt;&lt;br&gt;
The lab publishes 1000 &lt;code&gt;OrderPlaced&lt;/code&gt; messages spread across 100 order ids, each carrying a per-key sequence number, then binds one subscriber or the other and measures how long the backlog takes to drain. The handler blocks for a uniform random 250 to 1000 ms per message, mean 625 ms, standing in for an HTTP call or a database write. Everything else is identical between runs.&lt;/p&gt;

&lt;p&gt;First the ceiling, on paper: 1 divided by 0.625 seconds is 1.6 messages per second. Then the ceiling, measured:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Configuration&lt;/th&gt;
&lt;th&gt;Elapsed&lt;/th&gt;
&lt;th&gt;Throughput&lt;/th&gt;
&lt;th&gt;Speedup&lt;/th&gt;
&lt;th&gt;p50 wait&lt;/th&gt;
&lt;th&gt;p99 wait&lt;/th&gt;
&lt;th&gt;Ordering violations&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Inline on the callback&lt;/td&gt;
&lt;td&gt;635.9 s&lt;/td&gt;
&lt;td&gt;1.6/s&lt;/td&gt;
&lt;td&gt;1.00x&lt;/td&gt;
&lt;td&gt;320.8 s&lt;/td&gt;
&lt;td&gt;632.9 s&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Off the callback, 2 lanes&lt;/td&gt;
&lt;td&gt;328.7 s&lt;/td&gt;
&lt;td&gt;3.0/s&lt;/td&gt;
&lt;td&gt;1.88x&lt;/td&gt;
&lt;td&gt;162.7 s&lt;/td&gt;
&lt;td&gt;323.9 s&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Off the callback, 4 lanes&lt;/td&gt;
&lt;td&gt;223.6 s&lt;/td&gt;
&lt;td&gt;4.5/s&lt;/td&gt;
&lt;td&gt;2.81x&lt;/td&gt;
&lt;td&gt;90.5 s&lt;/td&gt;
&lt;td&gt;218.4 s&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Off the callback, 8 lanes&lt;/td&gt;
&lt;td&gt;102.5 s&lt;/td&gt;
&lt;td&gt;9.8/s&lt;/td&gt;
&lt;td&gt;6.12x&lt;/td&gt;
&lt;td&gt;47.2 s&lt;/td&gt;
&lt;td&gt;102.1 s&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Off the callback, 32 lanes&lt;/td&gt;
&lt;td&gt;43.6 s&lt;/td&gt;
&lt;td&gt;23.0/s&lt;/td&gt;
&lt;td&gt;14.38x&lt;/td&gt;
&lt;td&gt;20.6 s&lt;/td&gt;
&lt;td&gt;41.5 s&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The inline subscriber landed on 1.6 messages per second. The ceiling is not a metaphor, it is the measurement. Off the callback, 2 lanes halves the drain time, 8 lanes gives about 6x, 32 lanes gives about 14x, and an earlier sweep on the same setup kept scaling through 62 and 93 lanes. None of that was available to the inline design at any hardware size.&lt;/p&gt;

&lt;p&gt;p50 and p99 here are backlog-drain waits, not steady-state request latency. Every message was already sitting in the queue before the subscriber connected, so they mostly measure how long a message waited its turn.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The ordering objection&lt;/strong&gt;&lt;br&gt;
The reason a subscriber usually stays sequential is "we need messages for the same record in order." That is a real requirement and it does not require a single thread. Route by key. Same-key messages always land in the same lane and are handled in delivery order. Different keys run in parallel. The subscriber in the lab checks this on every message with a per-key sequence number, and across all five runs above, 5000 messages, it recorded zero ordering violations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two things to watch when you do this&lt;/strong&gt;&lt;br&gt;
Lane count needs key cardinality behind it. A key never spans lanes, so a lane with no keys assigned does nothing. The lab's first sweep used only 16 keys, and 8 lanes reached just 3.25x. The same 8 lanes with 100 keys, in the table above, reached 6.12x. Before picking a lane count, check how many distinct keys your real stream actually has.&lt;/p&gt;

&lt;p&gt;Blocking handlers above the ThreadPool minimum need help. The lab's handler uses Thread.Sleep, so each lane holds a real ThreadPool thread for the whole call. The pool's default minimum is Environment.ProcessorCount and it grows past that only through a throttled injection algorithm, so a lane count well above the core count ramps up over tens of seconds instead of running at full concurrency immediately. The lab's subscriber calls ThreadPool.SetMinThreads from the configured lane count before it binds the flow. An awaited async handler would not need this.&lt;/p&gt;

&lt;p&gt;And a caution in the other direction: lane count is not a CPU question for an I/O-shaped handler. It is how many calls you are willing to have in flight against whatever is downstream. That dependency's capacity, not your core count, is what should set it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway&lt;/strong&gt;&lt;br&gt;
If the delivery callback does the work, throughput is 1 divided by handler time, and no POC will ever show you that. Hand the message off, return, and let a keyed lane keep the order you actually need. Then put the number in front of the design before production does.&lt;/p&gt;

&lt;p&gt;Code, both subscribers, the load generator, and the benchmark reports: github.com/sthotakura/messaging-lab. The run behind the table above is reports/benchmark-20260905-191257.html.&lt;/p&gt;

&lt;p&gt;Verified against a local Solace PubSub+ Standard Docker broker via SolaceSystems.Solclient.Messaging on .NET 10. All numbers are from a single trial per configuration on one machine (32 logical CPUs); the same 32-lane configuration measured 19.8/s in an earlier sweep and 23.0/s in this one, so expect roughly 15% run-to-run variance. Speedup figures are the report's own ratios of rounded throughputs; the elapsed-time ratio for 32 lanes is 14.6x. The "one context thread, do not block the callback" claim is from Solace's current API developer guide, linked below.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>performance</category>
      <category>scalability</category>
    </item>
    <item>
      <title>A Rate Limiter That Knows Who's Calling</title>
      <dc:creator>Suresh Thotakura</dc:creator>
      <pubDate>Sat, 29 Aug 2026 19:47:21 +0000</pubDate>
      <link>https://dev.to/thotakura/a-rate-limiter-that-knows-whos-calling-i27</link>
      <guid>https://dev.to/thotakura/a-rate-limiter-that-knows-whos-calling-i27</guid>
      <description>&lt;p&gt;ASP.NET Core's rate limiter partitions however you key it. Key it on the caller, and one client's burst stops being every other client's problem.&lt;/p&gt;

&lt;p&gt;I built a small lab to get that wiring right end to end. &lt;/p&gt;

&lt;p&gt;Four stages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Authenticate. A custom header scheme validates the client and issues a client_id claim. Nothing downstream can partition per client until this exists.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Partition. The policy reads that claim and keys the partition by client id plus HTTP verb. A client's read budget and write budget are separate buckets, and so are two different clients'.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Limit. Fixed window: 10 requests per 10 seconds, queue limit 0. No waiting in line, you either get a permit or you don't.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Enforce. Request 11 in the window gets HTTP 429.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The client's profile decides which partition it lands in:&lt;/p&gt;

&lt;p&gt;Standard: limited on every verb.&lt;br&gt;
Premium: reads skip the limiter, writes still get 10 per 10 seconds.&lt;br&gt;
Unlimited: no limiter at all.&lt;/p&gt;

&lt;p&gt;The part worth remembering is the ordering. &lt;br&gt;
UseRateLimiter() has to run after UseAuthentication() and UseAuthorization(). Register it earlier and the policy executes before the client_id claim exists, so there is nothing to partition on. In this implementation that throws rather than quietly limiting on nothing, which is the failure mode you want: loud, not silent.&lt;/p&gt;

&lt;p&gt;Three calls do the wiring: AddRateLimiter() to register the policy, UseRateLimiter() to put it in the pipeline, RequireRateLimiting() to attach it to an endpoint group.&lt;/p&gt;

&lt;p&gt;Code, including the in-memory client fixtures and the Todo endpoints it protects: &lt;a href="https://github.com/sthotakura/rate-limiter-lab" rel="noopener noreferrer"&gt;https://github.com/sthotakura/rate-limiter-lab&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aspnet</category>
      <category>ratelimiting</category>
      <category>aspdotnet</category>
    </item>
    <item>
      <title>Sealed Isn't a Restriction, It's a Promise</title>
      <dc:creator>Suresh Thotakura</dc:creator>
      <pubDate>Fri, 28 Aug 2026 18:53:06 +0000</pubDate>
      <link>https://dev.to/thotakura/sealed-isnt-a-restriction-its-a-promise-99e</link>
      <guid>https://dev.to/thotakura/sealed-isnt-a-restriction-its-a-promise-99e</guid>
      <description>&lt;p&gt;Leaving a class open to inheritance is a design decision, not a default you can ignore.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core idea
&lt;/h2&gt;

&lt;p&gt;An unsealed class is a promise: every &lt;code&gt;virtual&lt;/code&gt; member can be overridden without breaking what the class guarantees. Most classes never meant to make that promise. They're just unsealed by default, because that's what &lt;code&gt;class&lt;/code&gt; gives you unless you say otherwise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Common mistake:&lt;/strong&gt; treating &lt;code&gt;sealed&lt;/code&gt; as "I don't want to think about subclassing" rather than "this type's invariants would break if someone could."&lt;/p&gt;

&lt;h2&gt;
  
  
  One override breaks the promise
&lt;/h2&gt;

&lt;p&gt;Here's the promise, a &lt;code&gt;BankAccount&lt;/code&gt; that refuses to go negative:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BankAccount&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;Balance&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Withdraw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Balance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;Balance&lt;/span&gt; &lt;span class="p"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here's the override that breaks it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RiskyAccount&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BankAccount&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Withdraw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Balance&lt;/span&gt; &lt;span class="p"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// no check&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing here is exotic. It compiles cleanly, and &lt;code&gt;RiskyAccount&lt;/code&gt; is a&lt;br&gt;
perfectly legal &lt;code&gt;BankAccount&lt;/code&gt; as far as the type system is concerned. Open&lt;br&gt;
one with a balance of 100 and withdraw 500:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;BankAccount&lt;/span&gt; &lt;span class="n"&gt;account&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;RiskyAccount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100m&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Withdraw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;500m&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Balance: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Balance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;F2&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Real &lt;code&gt;dotnet run&lt;/code&gt; output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Balance: -400.00
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The check on the left never ran. &lt;code&gt;virtual&lt;/code&gt; was an open invitation, and&lt;br&gt;
&lt;code&gt;RiskyAccount&lt;/code&gt; took it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Sealing turns a silent bug into a compile error
&lt;/h2&gt;

&lt;p&gt;Without &lt;code&gt;sealed&lt;/code&gt;, the code above compiles and produces a wrong answer at&lt;br&gt;
runtime; nothing points you at the problem until it's already in&lt;br&gt;
production. With &lt;code&gt;sealed&lt;/code&gt;, the same mistake becomes something the compiler&lt;br&gt;
catches before the code ever runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BankAccount&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;Balance&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Withdraw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Balance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;Balance&lt;/span&gt; &lt;span class="p"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RiskyAccount&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BankAccount&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// error CS0509: 'RiskyAccount': cannot derive&lt;/span&gt;
&lt;span class="c1"&gt;// from sealed type 'BankAccount'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sealing also makes &lt;code&gt;virtual&lt;/code&gt; on &lt;code&gt;Withdraw&lt;/code&gt; pointless, and the compiler&lt;br&gt;
enforces that too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BankAccount&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Withdraw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// error CS0549: 'BankAccount.Withdraw(decimal)' is a new virtual&lt;/span&gt;
&lt;span class="c1"&gt;// member in sealed type 'BankAccount'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both of those are real compiler errors, not paraphrased, verified against&lt;br&gt;
a live build.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Seal by default. Unseal only when you intend to support inheritance. Every&lt;br&gt;
unsealed class is an implicit promise to every future subclass that&lt;br&gt;
overriding won't break its invariants. Most classes never meant to make&lt;br&gt;
that promise, they're just unsealed because nobody had to actively choose&lt;br&gt;
not to be.&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>oop</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
