Sr. Content Developer at Microsoft, working remotely in PA, TechBash conference organizer, former Microsoft MVP, Husband, Dad and Geek.
159009 stories
·
33 followers

GitHub Admin UI + Billing API: Better together for smarter spend decisions

1 Share

As a GitHub administrator, you already have a strong place to start when somebody asks, “Why did our AI spend go up?” In Metered usage, you can see the change, choose the period, and group the data by organization or cost center.

That first investigation often leads to questions that are specific to your company. Finance may want a month-end report based on its own reporting calendar. An engineering leader may want to see whether an increase is spread across a team or concentrated among a few people. Answering those questions once is useful; answering them repeatedly calls for a reusable approach.

Use each surface for what it does best

The GitHub admin UI shows you where to look and gives you the controls to respond. The Billing Usage API helps you answer the recurring questions that are specific to your company. Neither replaces the other.

Together, they give administrators a practical loop: spot the change in Metered usage, understand it through a reusable API-powered view, and act with a targeted budget. That means better cost control without treating every user or team as the problem.

Let’s walk through this better-together approach using a common example: AI spend starts to rise, but the reason is not yet clear.

The question: Spend is up, but what is driving it?

Imagine that finance notices an increase in AI spend before the next close. It could be a sign that more developers are getting value from Copilot. It could also be one workload using far more than expected. At this point, nobody knows, and a broad restriction would be premature.

The GitHub administrator needs to help finance and engineering answer three practical questions:

  • Which part of the business is driving the increase?
  • Is the spend concentrated among a few users or broadly distributed?
  • Which control should change without disrupting everyone else?

The goal is not simply to reduce a number. It is to understand the increase well enough to protect useful work while addressing anything unexpected.

1. Start in the admin UI: Find the increase

The admin UI is the natural place to begin because it lets you explore the data before you decide what kind of report or control you need. Open Billing and licensing > Metered usage and select the relevant reporting period.

This first check matters. It confirms that the increase is real, shows when it happened, and gives you a shared starting point for the conversation with finance and engineering.

Metered usage establishes the increase and the period that needs investigation.

Fig 01: Metered usage establishes the increase and the period that needs investigation.

Narrow the increase by organization

An enterprise total tells you that spend changed, but not where to look next. Group the usage by organization to see which part of the enterprise contributed most to the increase.

Organization grouping narrows an enterprise-wide increase to an accountable business area

Fig 02: Organization grouping narrows an enterprise-wide increase to an accountable business area.

Suppose the octodemo organization stands out. You now know where to continue the investigation and which leaders can add context. You do not yet know whether the spend is justified, and that distinction matters. The increase could come from successful Copilot adoption, a migration, a seasonal workload, or an automated process that needs attention.

Connect the increase to a cost center

An organization can contain several teams, programs, and budgets. Grouping by cost center takes the investigation one step closer to the people who understand the work behind the spend.

Cost-center grouping identifies the financial owner of the increase

Fig 03: Cost-center grouping identifies the financial owner of the increase.

In this scenario, octodemo-org-cc has the largest increase. In only a few clicks, the admin UI has taken us from an enterprise-wide signal to the cost center that needs a closer look. For a one-time question, this may be enough.

Now imagine that finance asks for the same analysis every month, with a fixed reporting period and a ranking of spend by user. That is the point where the API adds value. It does not replace the investigation you just completed; it helps you repeat and extend it.

2. Continue with the API: Answer the repeatable question

The Billing Usage API gives you access to the data behind a more tailored report. You can use filters to match the period finance cares about, focus on the cost center you found in the UI, and build a view that can run again tomorrow or next month.

Billing usage endpoints and time filters provide the inputs for a reusable report

Fig 04: Billing usage endpoints and time filters provide the inputs for a reusable report.

Define the reporting question first

Before writing code, state the question the report needs to answer. In this example, it is:

Which users in the selected cost center account for the most net spend during this reporting period?

That one question keeps the report focused. It also determines the workflow:

  1. List the organization's members to establish the candidate users.
  2. Resolve which members belong to the selected cost center.
  3. Query organization AI credit and premium-request usage for those users and the selected period.
  4. Combine the results into a per-user total.
  5. Rank users and aggregate the result by cost center.

The prototype uses year, month, and optional day filters so the output matches the finance period. It also accepts a cost-center filter. Because the admin UI has already pointed us to octodemo-org-cc, there is no reason to start with every member of the enterprise.

Understand the per-user query pattern

There is one API behavior to understand before building the report. The organization billing endpoints return an aggregate when the user filter is omitted. To create a spend-by-user ranking, the workflow makes a filtered request for each selected user and usage type.

For example, this request asks for Eve's AI credit usage in July 2026:

curl -L \
    -H "Accept: application/vnd.github+json" \
    -H "Authorization: Bearer $GITHUB_TOKEN" \
    -H "X-GitHub-Api-Version: 2026-03-10" \
    "https://api.github.com/organizations/octodemo/settings/billing/ai_credit/usage?year=2026&month=7&user=eve"

The response contains one or more usage items, with amounts such as grossAmount, discountAmount, and netAmount. The prototype adds the netAmount values to calculate Eve's AI credit total for the period. It then runs the equivalent premium-request query and combines the two totals.

We can now see one user's contribution during the same period we investigated in the UI. Repeating the request for the members of the selected cost center gives us the ranking that finance asked for.

For a production workflow, a few practical details matter:

  • Limit the candidate list to the cost center under investigation.
  • Paginate organization membership and cost-center results.
  • Use bounded concurrency instead of sending every request at once.
  • Record partial failures rather than silently treating them as zero spend.
  • Keep an audit record of when the data was pulled and transformed.

For a daily check, the report can use a narrow period and write a timestamped output. At finance close, the same workflow can produce the month-end rollup. The question stays the same; only the reporting window changes.

Reveal concentration that totals can hide

The result is a custom Spend by User view that brings the organization, cost center, reporting period, AI credit usage, premium-request usage, and total net spend into one place.

Custom Spend by User dashboard

Fig 05: A company-specific dashboard exposes per-user concentration inside the selected cost center.

In the illustrative data, the octodemo organization has 22 users and $3,651 in total net spend for July 2026. The octodemo-org-cc cost center accounts for $2,700 of that amount. Two users stand out:

User AI credit net spend Premium-request net spend Total net spend
eve $900 $600 $1,500
adam $600 $400 $1,000

Together, Adam and Eve account for $2,500 of the $2,700 attributed to that cost center. That is approximately 93% of its total in this example.

These figures are demonstration data, but they show why the extra view is useful. Instead of reacting to a $2,700 cost-center total, the administrator can talk to the owners of two workloads and understand what the spend supported.

Concentration does not automatically mean waste. Adam and Eve may be doing approved, high-value work. The dashboard tells the business where to ask the next question; the people involved provide the context needed to answer it.

3. Return to the admin UI: Choose the right control

The API has helped us understand the increase, but it does not make the decision for us. Return to Billing and licensing > Budgets and alerts to review the available controls and choose the narrowest one that fits what you learned.

Budget scopes turn the investigation into a targeted governance decision

Fig 06: Budget scopes turn the investigation into a targeted governance decision.

Set a cost-center user-level baseline

A cost-center user-level budget applies the same per-user amount to every current and future member of that cost center. This is useful when the group needs a different baseline from the rest of the enterprise.

For example, the administrator might give octodemo-org-cc additional per-user headroom because its work legitimately uses more AI credits. This avoids raising the universal user-level budget for everyone.

A user-level budget counts both included and paid AI credit usage. It is always a hard stop for the individual. It does not reserve part of the shared pool, and it does not replace the cost center's paid-usage budget.

Preserve justified exceptions

If Adam or Eve has an approved role that requires more capacity, an individual user-level budget can replace the cost-center baseline for that person. The exception stays limited to the person who needs it instead of increasing the budget for the whole cost center.

Fig 07: Cost-center baselines and individual overrides preserve useful work without widening access for everyone.

The precedence is straightforward:

  1. An individual user-level budget overrides the cost-center user-level budget.
  2. The cost-center user-level budget overrides the universal user-level budget.

In practice, you can set a universal baseline, add more headroom for a cost center with a clear business need, and use individual overrides for documented exceptions.

Why the UI and API work better together

At this point, the better-together pattern becomes clear:

  • Metered usage supports interactive discovery.
  • Billing Usage API supports repeatable, company-specific analysis.
  • Budgets and alerts supports targeted policy decisions.

Each surface does the job it is best suited to do. The UI makes it easy to explore and manage GitHub. The API lets you repeat a company-specific analysis without rebuilding it by hand. Used together, they give finance, engineering, and administrators the same evidence before a control changes.

Make it part of the operating rhythm

A useful dashboard should lead to a useful conversation. Decide who receives the report, how often they review it, and what happens when a user or cost center stands out.

For example:

  • Run a daily pull to detect unusual changes early.
  • Produce a month-end rollup aligned to finance close.
  • Route cost-center summaries to the relevant business owner.
  • Review high-consumption users with engineering before changing limits.
  • Record approved individual overrides and revisit them regularly.

Over time, the conversation can move from “Who spent this?” to “What outcome did this spend support, and does the current policy still fit?”

When the same users repeatedly appear at the top, leaders can inspect the workload, remove waste, validate business value, or approve more capacity. When usage becomes broadly distributed, the cost-center baseline may need adjustment instead. The report makes those patterns visible over time.

The better-together workflow at a glance

The story above introduces each surface when it becomes useful. This table summarizes their roles.

Surface Primary role Best used for Important limitation
Metered usage Interactive investigation Finding the affected period, organization, and cost center Manual exploration is not a reusable company-specific report
Billing Usage API Programmatic usage retrieval Scheduled reporting, time-sliced analysis, and per-user views Per-user attribution requires filtered requests and careful handling of pagination and failures
Custom Spend by User view Company-specific interpretation Ranking users and aligning usage to internal ownership Concentration is evidence to investigate, not proof of waste
Budgets and alerts Governance controls Cost-center baselines and individual overrides A broader budget cannot override a user who has reached their ULB

The practical takeaway is simple: begin with exploration, automate only the question worth repeating, and adjust policy after the data has context. That sequence keeps governance precise while preserving useful AI work.

Learn more

Read the whole story
alvinashcraft
just a second ago
reply
Pennsylvania, USA
Share this story
Delete

Chrome DevTools Connect for WebStorm: Your AI agent can now interact with the browser

1 Share

Frontend development has historically meant working in three separate environments: a design tool for specs and prototypes, the IDE for coding, and a browser to check if everything works. A few days ago, we released the new plugin Figma Connect for WebStorm, which eliminated the first context switch. Design intent, component specs, and design tokens flow directly from Figma into WebStorm, so the agent starts with the actual design, not a description of it.

That left the second switch untouched. The agent generates code, and then you leave the IDE, open the browser, check what’s rendering, catch something broken, describe it back to the agent, and repeat. The browser was still a separate environment – one that the agent couldn’t see.

Now, Chrome DevTools Connect eliminates the need for that second switch. It ships as a bundled Chrome DevTools CLI skill in WebStorm 2026.2.1, helping an AI agent interact with Chrome using the full power of Chrome DevTools. The first time the agent reaches for it, WebStorm prompts a one-time package install, and after that, it’s automatic.

Get started

What the agent can see and do in the browser

When you’re working on a UI with an AI agent, it can open Chrome, inspect what’s rendering, read console logs and network requests, take screenshots, and interact with the page directly – without you leaving the IDE or narrating what you see.

The agent reaches for the browser when it decides runtime verification is needed, or when you ask it to. No additional setup, MCP wiring, or researching which browser automation tool plays well with your setup.

It’s built in partnership with Google’s Chrome DevTools team, using the Chrome DevTools CLI.

What this changes in practice

Consider a bug in a multi-step checkout flow: The order summary shows stale prices after a user goes back and updates their cart. The component renders correctly on first load, and nothing in the code looks wrong. The bug only surfaces after a specific sequence: Add item → proceed to checkout → go back → change quantity → proceed again.

Previously, reproducing this bug meant clicking through that sequence manually every time. You’d spot the stale price, switch to the IDE, describe what you saw, wait for the agent’s fix, then click through the whole sequence again to verify. If the fix was off, you’d repeat. The agent was working from your description – you were the one doing the clicking.
With Chrome DevTools Connect, you tell the agent: “The order summary shows stale prices if you go back and change the cart contents, so fix it and verify the fix in the browser.”

The agent navigates through the flow, reproduces the bug, reads the stale state from the console, traces it to a missing dependency, applies the fix, and clicks through the sequence again to confirm it’s gone. You stay in the loop for decisions but stop being the relay between the IDE and the browser.

Design → code → browser workflow

Figma Connect for WebStorm was the first part of the design-to-code workflow which included design into the IDE at the start. Now, Chrome DevTools Connect is the second, with browser validation coming at the end.

The full design → code → browser workflow now takes place entirely inside WebStorm – no switching required.

If you enjoyed this, stay tuned because there are more integrations on the way!

Read the whole story
alvinashcraft
13 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

Rebuilding communities

1 Share

The video Constructing the Revolution talks about how to start prefiguring the future you want today.

(Heads up: the middle section of the video gets into some pretty heavy territory near the middle.)

Early on, there’s some important conversation about rebuilding and repairing communities that have been shattered by a system that deliberately pits us against one another.

All hierarchies of power desire to keep us separate, competing, and individualistic, because because we’re easier to exploit that way.

Dismiss the idea that your task is only to build councils of like-minded individuals. You have to start from a presumption that those around you are salvageable, until the time comes that they prove to you that they’re not.

It’s by this rebuilding of our shattered communities that we might rediscover solidarity with our fellow humans.

I’ve definitely noticed how much the last few years have really intensified this tendency.

There’s been so much fuckery from so many people for so long that I’m quick to cut people off, and have a very low tolerance for anyone still holding conservative values.

But that’s exactly what the rich and powerful want.

This was a good reminder that the kind of anarchist community I want to live in has people with lots of different beliefs and values, and so long as they don’t infringe on other peoples’ right to a rich and fulfilling life, that’s not just OK, but desirable.

Read the whole story
alvinashcraft
41 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

Vertical slices, their ownership and external dependencies

1 Share

cover

A few questions about Vertical Slices come up again and again, and they’re good ones.

  • If a slice cuts through all the layers, do we get a table per slice?
  • If I’ve split the application into seven areas, are those seven bounded contexts, or seven slices of one? And does the answer change what they’re allowed to know about each other?
  • How can a “verify order” slice check what a “register order” slice wrote, if the two aren’t supposed to know about each other?
  • Where does the fetching code live when one screen needs data owned by another module, and the dependency rules block every place we could put it?
  • Should a UI component know about the API call a neighbouring component makes?

For me, these are all versions of one question: what does a slice do when it needs something from the outside world?

Calling every area of an application a bounded context sets the bar for separation as high as it can go, because contexts are genuinely meant to be autonomous. Once you’ve done that, connecting any two of them looks like a violation, and the other questions have no legal answer.

The assumption doing the damage rarely gets stated because it feels too obvious: a slice ought to be self-contained, so needing something from elsewhere means the cut was wrong. I held it myself for a while.

I’ve written before about how to slice the codebase effectively and shown a worked module, and I listed “you can’t share code between slices” as one of the myths that grew around the pattern. What I didn’t do was show the positive answer. Minimise isn’t zero, so what does the non-zero look like once you type it out?

Vocabulary is where this gets tangled, so let me start there.

Slice, module, context

Three words that get used interchangeably, and I think a lot of the pain comes from that.

A vertical slice is one piece of functionality, cut through the whole application. For me, a slice is more a function than an entity. “Verify a transport order” is a slice. It has a way in, some business logic, and whatever it reads and writes. If you’re thinking of it as a thing with a lifecycle, you’re probably thinking of an entity, which is a different concept that lives within the slice’s reach rather than being the slice.

A module is a logical grouping of slices. Orders is a module. It holds registering an order, verifying it, confirming it, and listing what’s pending. The grouping is a judgement call, and the criterion I use is what changes together.

A bounded context, in DDD terms, is a linguistic barrier. It’s a set of functionality that the business uses the same vocabulary for, distinct from other contexts. In a given context, a word always means the same thing. Across contexts, the same word can mean something else entirely.

A frontend and a backend aren’t two bounded contexts; they’re two deployment targets. You can have one deployment for multiple contexts, and a single bounded context with multiple deployments. If a frontend feature is part of the bounded context and has backing WebAPI, then the business should use the same words across both. Two product listings that differ in which fields they show aren’t two contexts either, since nothing about the vocabulary changes between them. Draw the line in either place, and you get the terminology without the boundary (or just a pure technical boundary), and then wonder why the “contexts” need constant coordination.

Seven features of one application are almost always slices, or at most modules, sitting inside a single context. That’s good news, because it means they were never obliged to be autonomous.

I’ll be honest: I don’t love the term “bounded context”. It’s imprecise in the original books and even more misleading in practice, because most people encounter it third-hand and take it to mean “a big folder we agreed to keep apart”. If the word causes arguments on your team, drop it and talk about which functionalities share a vocabulary. That usually works better.

Labels aside, what I do in practice is start from the functionalities we have to deliver, group them logically into components, and check that grouping against the UX map. Technological splits (a layer for data access, a layer for services, one for the frontend and one for the backend) have always ended badly in my projects. Business grouping usually held up longer.

Slices boundaries in practice

Let’s use the transport order. It gets registered, then verified, then confirmed, then settled. Verification means checking that the contractor and the driver actually exist in the carrier’s own system, which is an external API we don’t control.

It’s tempting to model this as one orders row with a status column that moves registered → verified → confirmed, updated through a generic endpoint. That’s how it usually starts, and the trouble begins there, because the endpoint that changes status also changes the pickup address and the contact phone. The system records that the order is verified. It doesn’t record that anybody verified it.

Greg Young made this argument better than I will in Task-Based UI: when the client posts data-centric structures back and forth, the domain has no verbs, and the user’s intent is lost on the way in. His point is that the client should tell the server to do something, so that the intent becomes the exact task for the backend expressed by the message we sent (rather than being inferred from a combination of fields mixed with the current state).

Naming the operation is also what gives you something to slice along. If every operation is “update the order”, you have one feature and nothing to divide. Once you have VerifyOrder, ConfirmOrder, RejectOrder, you have folders, and each folder is named the way the business names the operation. This holds for plain CRUD systems too. The name is the value, and the underlying implementation can be a single UPDATE.

So we get:

📁 orders
    📁 verifying-order
    📁 confirming-order
    📁 registering-order
    📁 pending-orders

Which leaves the question I started with. Verification needs the carrier’s system, and confirming needs to know what verification established. Where does that go?

The shape I use is a handler taking two arguments: its dependencies, then the message.

// orders/verifying-order/verifyOrder.ts

export type ContractorStatus = 'Active' | 'Unknown' | 'Suspended';
export type DriverStatus = 'Licensed' | 'Unknown' | 'Expired';

// Dependencies
export type CheckContractor = (id: ContractorId) => Promise<ContractorStatus>;
export type CheckDriver = (id: DriverId) => Promise<DriverStatus>;

// Handler
export const verifyOrderHandler = async (
  {
    getOrder,
    saveOrder,
    checkContractor,
    checkDriver,
  }: {
    getOrder: GetOrder;
    saveOrder: SaveOrder;
    checkContractor: CheckContractor;
    checkDriver: CheckDriver;
  },
  command: VerifyOrder,
): Promise<void> => {
  const order = await getOrder(command.orderId);

  const [contractor, driver] = await Promise.all([
    checkContractor(order.contractorId),
    checkDriver(order.driverId),
  ]);

  await saveOrder(verifyOrder({ ...command, contractor, driver }, order));
};

The first parameter is dependencies, and I think about it the same way I think about React props. The component declares what it expects; whoever renders it decides what to pass. Same here: the handler declares what it expects, and it has no opinion about where those functions come from: the same module, a different module, an HTTP client, or a stub.

The business logic underneath takes everything as data and returns a decision:

// orders/verifying-order/verifyOrder.ts

// Business logic
export const verifyOrder = (
  command: VerifyOrder & {
    contractor: ContractorStatus;
    driver: DriverStatus;
  },
  order: Order,
): Order => {
  if (order.status !== 'Registered')
    throw new Error(`Cannot verify an order in ${order.status} state`);

  if (command.contractor !== 'Active')
    throw new Error(`Contractor is ${command.contractor}`);

  if (command.driver !== 'Licensed')
    throw new Error(`Driver licence is ${command.driver}`);

  return {
    ...order,
    status: 'Verified',
    verifiedBy: command.verifiedBy,
    verifiedAt: command.now,
  };
};

Nothing here is fetched, so testing it means passing values in and checking what comes out. No mocks, no container, no database.

There’s no ITransportManagementSystem interface here with fifteen methods. Pragmatically, you can pull the whole thing in, and plenty of codebases do, but I’ve come to prefer narrowing it: two functions, three cases each, defined in this folder, in this slice’s language.

The carrier’s system almost certainly has a richer model of a contractor than the three cases, including credit terms, insurance validity, certificate expiry dates, and territorial permissions. Verification only needs to know whether this order can proceed. Declaring the narrow type keeps the slice in its own vocabulary rather than importing someone else’s, which is the bounded-context idea applied on a much smaller scale.

Duck typing and dependencies

Nothing in the codebase declares that it implements CheckContractor. There’s no implements clause anywhere. Any function of a compatible shape satisfies it. That means the type can live with the consumer, the slice that needs it, rather than with whoever ends up providing it.

So you can write the need, the handler, and the tests before anyone has decided who will serve it. Whether checkContractor becomes an HTTP call, a query against a table we replicate nightly, or a function returning 'Active' for the first three weeks stays open. When an external shape doesn’t match, you fit it or remap it at the point of supply, and neither side changes.

Golang bets its whole dependency story on this. Interfaces are satisfied implicitly, and the convention is that the consumer declares the interface it needs, sized to its use. Hence the proverb about the bigger interface being the weaker abstraction. io.Reader is one method, and half the standard library composes through it.

Structural typing gives us that in TypeScript, and I think it’s underused by people arriving from C# and Java, where the habit is to define a nominal interface and hand it around.

So where do the dependencies come from?

Somewhere near the entry point, you create the real things once, build the small functions the handlers asked for, and pass them in:

// apps/api/composition.ts
const db = drizzle(process.env.DATABASE_URL!);
const carrier = carrierApiClient(process.env.CARRIER_API_URL!);

const contractorStatus =
  (client: CarrierApiClient): CheckContractor =>
  async (id) => {
    const contractor = await client.getContractor(id);

    if (!contractor) return 'Unknown';

    return contractor.suspendedAt ? 'Suspended' : 'Active';
  };

export const orders = {
  getOrder: getOrderFrom(db),
  saveOrder: saveOrderTo(db),
  checkContractor: contractorStatus(carrier),
  checkDriver: driverStatus(carrier),
};

That mapping function is where two vocabularies meet, and I like having exactly one place where that happens. Everything the carrier knows about a contractor collapses into three cases, in a file you can read in ten seconds.

The routes are then thin:

// apps/api/routes.ts
app.post('/orders/:id/verification', async (req, res) => {
  await verifyOrderHandler(orders, {
    orderId: req.params.id,
    verifiedBy: req.user.id,
    now: new Date(),
  });

  res.status(204).end();
});

That’s the mechanism: partial application, done by hand, in a file whose job is to know about everything so that nothing else has to. No container, no registration, no lifetime scopes. In a monorepo, this file lives in apps/, and packages/ holds slices that declare but never resolve. If you’re using Nx, this is the composition point its boundary rules exist to protect. In other environments, where Dependency Injection Containers are out of the box, you can still use the same way; in .NET, you can use the same pattern by injecting dependencies explicitly even to methods.

The tests use the same shape:

const contractor = (status: ContractorStatus): CheckContractor => () =>
  Promise.resolve(status);

test('refuses to verify an order for a suspended contractor', async () => {
  const store = inMemoryOrderStore([registeredOrder]);

  await expect(
    verifyOrderHandler(
      { ...store, ...carrierStubs, checkContractor: contractor('Suspended') },
      verifyCommand,
    ),
  ).rejects.toThrow('Contractor is Suspended');
});

There’s no mocking framework here, because there’s nothing to mock. You can pass an in-memory implementation, a stub, or a real client pointed at a sandbox. The handler can’t tell the difference.

Everything outside the slice is external

From inside a slice, there’s one category of thing: external. Another slice next door, the parent module, a different module, a third-party API, the database. All the same. The slice states a function type and doesn’t ask where the implementation comes from.

That keeps the slice movable. Because it defines its own dependencies, changing the logical grouping later is a matter of what you inject, not a rewrite. If verification turns out to belong in its own module, or gets extracted into a service, the handler stays as it is. Testing in isolation comes out of the same property, as the test above shows, and I get it as a side effect rather than designing for it.

None of this is about hiding the coupling. For me, independence isn’t a value in itself. I’d rather know the connections I need to have and be able to look at the code to see what it depends on and what it does. Hidden dependencies are still there, only harder to find. What I’m optimising for is cohesion, and explicit dependencies serve that.

What about the module’s public API?

If you’ve read my Architecture Weekly piece on VSA, you’ll have seen me recommend an api.ts per module, exposing what’s public and hiding the rest. That may seem to contradict what I’ve just described.

They point in opposite directions across the same boundary. api.ts is what a module offers: the surface it’s willing to support and that the module owns. CheckContractor is what a slice asks for: a need, owned by the consumer, expressed in the consumer’s terms.

You want both, because they protect against different things. Without the module API, everything is reachable, and any internal change can break a caller you didn’t know you had. Without consumer-declared needs, the consumer is coupled to the shape of whatever the provider decided to expose, including the parts it never calls.

The composition root is where the two meet. It takes what the module offers and adapts it to what the slice asked for. That adapter is a few lines, it lives in one place, and it’s the only code that knows both vocabularies.

Cycles stop being a problem. Orders needs contractor standing. Pricing needs order history to work out volume discounts for the same contractor. That’s a genuine mutual need, and if both modules import each other, you have a cycle. The compiler may tolerate it. Your boundary rules probably won’t. This is the situation where the tooling forbids two modules at the same layer from referencing each other, and it’s easy to conclude from that the design must be wrong.

The usual escape is to extract the shared parts into a common module. That works twice; then the common module becomes the place everything ambiguous lands, and changing it means changing everything.

But there’s no cycle if neither module imports the other. Orders declares CheckContractor. Pricing declares its own:

// pricing/volume-discounts/orderHistory.ts
export type CompletedOrder = Readonly<{
  completedOn: Date;
  netValue: Money;
}>;

export type GetCompletedOrders = (
  contractorId: ContractorId,
  since: Date,
) => Promise<CompletedOrder[]>;

Two fields, because that’s what a volume discount calculation uses. The composition root supplies a function that queries orders and maps the result down to that shape.

Most module-level cycles I’ve run into were a shared concept whose owner hadn’t been decided yet. Declaring narrow needs lets you defer that decision rather than resolve it early and wrong. If it later turns out that contractor standing genuinely belongs in one place with a single definition, you’ll know more by then, and moving it is cheap because only the adapters point to it.

If your dependency rules forbid the arrangement your domain wants, it’s worth checking whether they’re describing your design or your import graph. Rules that block a module from reaching another module are useful. Rules that block it because two files sit at the same “layer” are enforcing a layering you may have already outgrown.

Two entry points, one feature

What if the same operation can be triggered two ways?

An order gets confirmed by a dispatcher through the UI. It also gets confirmed automatically when the carrier’s system reports the assignment accepted. Same rule, different trigger, different surrounding logic.

Both files go in confirming-order/, side by side. I showed this pattern in Vertical Slices in practice using an API endpoint and an external booking event that share a single command handler. With dependencies declared per handler, the event-triggered one looks like this:

// orders/confirming-order/confirmOrderOnAssignmentAccepted.ts

export const confirmOrderOnAssignmentAccepted = async (
  { getOrder, saveOrder }: { getOrder: GetOrder; saveOrder: SaveOrder },
  event: AssignmentAccepted,
): Promise<void> => {
  const order = await getOrder(event.orderId);

  await saveOrder(
    confirmOrder(
      {
        orderId: event.orderId,
        confirmedBy: SystemUser,
        now: event.acceptedAt,
      },
      order,
    ),
  );
};

The two handlers don’t have the same dependencies. This one has no carrier check at all, since the event came from the carrier and we already have the answer. The dispatcher-triggered version loads more, because it starts with less. The business logic is shared; the application logic isn’t, and each entry point declares what it actually uses.

The same applies to what you reuse across slices. I’d reuse a policy or a calculator, say a pure function like settlementFor(order, tariff) used by both confirmation and settlement, before I’d reuse a whole handler. The pure function is a function of its arguments and cheap to share. Handlers differ in what they load, check and record, and that’s the part that tends to change.

Vertical Slices and Database

Back to the table per slice. Three separate things get conflated in that question.

Business logic goes per entity or aggregate. The rules about what states an order can be in and which transitions are legal belong to the order. One place, and every slice that decides about an order goes through it. Slices don’t each get a private notion of what an order is.

Read models go per query. This is where a table per feature is right. The dispatcher’s pending-orders board needs order number, contractor, pickup window and verification state, sorted by pickup time. Build a table that answers that. The settlement report needs something else and gets its own. Resist making a single query serve five screens by expanding columns.

Database schemas go per module. One for orders, one for pricing. A slice is a feature, not a persistence boundary. A schema per slice gives you migrations that correspond to nothing, and joins across five schemas to draw a single screen.

You can do all of this with one status column and an ORM. If you want the four distinct operations that currently collapse into status = 'cancelled' to stay distinguishable, appending them as facts is what Event Sourcing offers, and it composes well with this. Nothing above depends on it.

Vertical Slices and Frontend

Don’t force a 1:1 mapping between the UI and the backend. One screen is routinely composed of data gathered from several modules, and one user action can trigger operations in several. That mismatch is normal. GraphQL exists partly as an attempt to solve it, whatever you make of that as a solution.

A dispatcher board showing pending orders, contractor status and a confirm button touches three slices across two modules. Two reasonable arrangements:

Compose at the page. The page fetches from several endpoints and arranges the results, holding no business logic of its own. Components receive what they need as props and know nothing about where they came from, for the same reason that handlers take dependencies as arguments. A component that takes props is easy to move and easy to feed with a different implementation.

Or write a backend-for-frontend. If the screen needs it in one request, make a slice whose job is to serve that screen, depending on the others and stitching them together. It’s still a slice; it’s named after a screen because that’s honestly what it is.

Which one depends on how much the round-trip costs you and how stable the screen is. Both beat growing one endpoint until it serves every screen you have.

The backend usually knows which operations are currently available for a given order, and the frontend is often re-deriving that from status fields. Returning the available actions with the data keeps that decision in one place. That’s the part of HATEOAS I find useful, without the rest of the ceremony.

Splitting the frontend by kind of thing tends to look suspicious to me. Two listings that differ only in which fields they show are usually one feature with a parameter. Splitting by market or country is more often real, because it genuinely is a different application with different rules. It’s hard to judge from a description; what I’d do is map the functionalities first and see which ones actually change together.

TLDR

The question I started with was what a slice does when it needs something it doesn’t own. My answer is that it declares the need and leaves it to something else to decide where it comes from.

Which comes down to:

  • Everything outside the slice is external, whether it’s the next folder or another system.
  • Declare narrow function types where they’re used, in your own vocabulary, rather than importing a wide interface.
  • Compose by passing functions in, in one file, near the entry point.
  • A module’s public API and a slice’s declared need are different directions of the same boundary, and the composition root adapts between them.
  • Reuse policies and calculators; let handlers duplicate.
  • Business logic per entity, read model per query, schema per module.
  • Don’t expect the frontend to mirror the backend.

None of this needs a framework, a container or a particular database. It’s more about a convention applied consistently.

And your first grouping will still be wrong somewhere. Mine usually is. That’s fine, as long as being wrong stays cheap, which is the argument for removability over maintainability and for keeping the couplings visible rather than tucked away. A slice that states its dependencies is one you can move.

And the cherry on top: this approach helps LLM agents too. Everything a feature needs sits in one folder, with a handful of function types crossing the boundary. To change how verification works, you open verifying-order. The command, the rules, both entry points and the tests are in it.

That was always the argument for grouping changes together: it reduces how much a person has to hold in their head. The same property determines how much can fit in a context window and how much of a codebase an agent has to read before it can safely change one behaviour. A layered structure that needs five folders touched for one feature costs an agent the same way it costs us, faster.

So it’s an old argument that happens to have got more valuable.

Cheers!

Oskar

p.s. Ukraine is still under brutal Russian invasion. A lot of Ukrainian people are hurt, without shelter and need help. You can help in various ways, for instance, directly helping refugees, spreading awareness, putting pressure on your local government or companies. You can also support Ukraine by donating e.g. to Red Cross, Ukraine humanitarian organisation or donate Ambulances for Ukraine.

Read the whole story
alvinashcraft
1 minute ago
reply
Pennsylvania, USA
Share this story
Delete

Why the GitHub Copilot app became my daily driver

1 Share

Disclaimer: This post was originally published on Azure with AJ and has been reproduced here with permission. You can find the original post here. 

I did not want to like the GitHub Copilot app. I had VS Code tuned exactly how I wanted it, extensions curated over years, keybindings in muscle memory, and a colour theme I will defend to the death. Handing that over for a new desktop app felt like swapping a lightsaber I had built myself for one someone handed me in a shop. 

Fast forward and it is open on my machine every single day, sitting alongside VS Code rather than replacing it. VS Code is still where I go for heavy code development, the hands on work where I want the folder tree, the extensions and the debugger. The Copilot app is where I direct the work. 

This is not a scorecard, it is my view on why the GitHub Copilot app is now a valid place to do serious work, the good, the awkward and the genuinely annoying. Everything factual here comes from GitHub’s own documentation, everything opinionated is mine. 

What the GitHub Copilot app actually is

Before the feelings, the facts. The GitHub Copilot app is a desktop application purpose built for agent driven development, available on macOS, Windows and Linux, and available across all Copilot plans. It is built on GitHub Copilot CLI and integrates natively with GitHub, so repositories, branches, issues, pull requests and CI results work out of the box. 

The important word in that description is agents, not editor. GitHub is explicit that it exists so you can direct multiple agents across parallel workstreams instead of context switching between terminal, IDE and browser tabs. Each session runs in its own isolated workspace with a dedicated git worktree and branch, and you can pick where a session runs, a new working tree, your local repository, or a cloud sandbox. 

If you want the wider landscape view, I compared it against other agentic surfaces in Agentic Workflows vs Scout vs Copilot App. 

The part where I resisted

Early on it was mild frustration. I kept asking myself the obvious question, why would I use this when VS Code already has Copilot in it? 

The answer was not obvious, because I was using the app wrong. I started with chat, treating it as a fancier chat window, which is the least interesting thing it does. Then I kicked off real agentic work and the discomfort arrived properly. No folder tree down the left. No extensions bar. No terminal sitting where my eyes expect it. My hands kept reaching for shortcuts that were not there. 

That reaction is worth naming, because it is not a product flaw, it is a model mismatch. An IDE optimises for you writing lines. The Copilot app optimises for you directing work and reviewing outcomes. The sidebar tells the story, My work, Automations, Search and Sessions. Not a file explorer in sight, because files are what the agent is dealing with, not you. 

I was looking for a cockpit and had been handed the war room on Yavin 4. Once I stopped mourning my folder tree, things got interesting fast. 

The turning point: from curiosity to daily habit

The shift did not happen in one dramatic moment. It happened over a handful of real use cases, one after another, a refactor here, a documentation sync there, adding a new skill in a repo on a Friday afternoon that I could not be bothered branching for manually. Nothing individually convinced me, but the tally added up quickly, and I noticed I was reaching for the app before I had consciously decided to. 

Part of what made experimenting low risk is session modes. You choose how much rope the agent gets, and you can change it mid flight: 

  • Interactive, the agent suggests changes and waits for your input. 
  • Plan, the agent proposes a plan you approve before it executes. 
  • Autopilot, the agent writes code, runs tests and iterates on its own. 

 

I will be upfront that Plan mode has not been my own habit, my sessions have mostly lived in Interactive and Autopilot. But it is a genuinely useful option if you want a checkpoint before an agent starts touching your repository, and combined with a model picker and reasoning effort control per session, the range of control on offer is well thought out regardless of which mode you settle on. 

What changed is that it slowly became essential for real development work rather than something I dipped into occasionally. When I have three streams of work in flight across two repositories, this is where I sit. When one of those streams needs me elbow deep in the code itself, I drop into VS Code, do the work properly, then come back. 

Automations are the sleeper feature

If I had to pick one thing that moved the app from “interesting” to “essential”, it is automations.

Automations let you save recurring agent tasks and run them on a schedule or on demand. In the app you get an Automations tab in the sidebar, and each automation shows its name, schedule, associated repository and last run status. There are two flavours, local automations that run from your environment, and cloud automations that run in a cloud environment so they still fire when your machine is off. 

Triggers are refreshingly simple: 

  • Manual, run it whenever you want with the play button on its card. 
  • On a schedule, hourly, daily or weekly. 
  • When an issue is created, with an optional search query filter so you only catch the issues you care about. 

 

For cloud automations you also select the tools Copilot may use, such as pushing changes, updating issue labels or creating a pull request. Selecting only what the task needs is the same least privilege discipline we apply everywhere else, and it is refreshing to see it as a first class dropdown rather than a buried policy. 

My favourite detail is the smallest one. While troubleshooting I was able to schedule a retry for 24 hours later while I was waiting for a system change to propagate, meaning one off jobs can get saved easily rather than retyped from scratch every time I want to repeat them. 

The other pleasant surprise was stumbling onto an inbox style view in the app that surfaces reminders and nudges me to run something when it is actually relevant, rather than leaving automations to fire blind on a timer and hoping I remember to check the results. It is a small addition, but it is the difference between an automation running while I am not looking and one that taps me on the shoulder at the right moment.

Multiple accounts, or why my cross org life got easier

Working across a personal account and multiple client organisations has always meant an authentication tax. Sign out, sign in, re authorise, forget which identity you were in, push to the wrong remote, feel shame. 

Being able to work across accounts and set the identity per repository and session removes what has honestly been a barrier to entry for years. In practice it is the difference between picking up a cross org task immediately and putting it off until I have the energy for the ceremony. 

The rough edges

With that said, here is what still irritates me. Both feel like maturity problems rather than design problems. 

Customisation still pulls you out of the UI. The docs say you can add and manage agent skills and MCP servers in app settings, and anything already configured for your repositories or Copilot CLI is picked up automatically. That is true, and there is a catalogue of popular MCP servers. But the moment you go beyond the catalogue and need to provide in depth customisations , you are back in local configuration files, restarting things and guessing why a server did not appear. For an app whose whole pitch is “stay in one place”, the customisation path is the one journey that keeps sending me elsewhere. 

No terminal until a session exists. Sessions own the workspace, which makes sense architecturally, since each one gets its own worktree. It also means the terminal is not there when you first open the session. If your instinct is to poke around a session before deciding what to do, that instinct is temporarily homeless. Quick chats help for questions, because they open a conversation without creating a branch or worktree, but they are not a shell. 

Neither is a deal breaker. Both are the kind of thing I expect to read about in a changelog within a couple of releases. 

Credits and common sense

One practical note. Agent sessions consume AI credits, and GitHub publishes sensible guidance on optimising usage, match model capability to task complexity, use Plan mode to validate scope before burning effort, use quick chats for early exploration, and start a fresh session when you switch tasks so you are not dragging irrelevant context along. 

Treat autonomous runs like cloud spend. Start narrow, watch the usage, expand what clearly pays for itself. 

Conclusion: who should actually switch

The GitHub Copilot app did not replace my IDE and I no longer expect it to. VS Code is still my lightsaber for heavy code development. The Copilot app is the command deck, and it replaced the orchestration layer that used to live in my head, spread across terminal tabs, browser windows and half remembered intentions. 

I would recommend it to you if: 

  • You regularly run more than one stream of work at a time. 
  • You live across multiple organisations or accounts and are tired of the sign in shuffle. 
  • You have recurring repository chores that would happily run on a schedule. 
  • You are comfortable directing and reviewing rather than typing every line. 

 

I would hold off if you are mostly doing focused single threaded work in one repository, or if your workflow depends heavily on IDE extensions. These are not the droids you are looking for, and the app is not trying to win that fight anyway. Keep VS Code for the deep code work and let the app handle everything around it. 

Start small. Install it, connect one repository, run one Plan mode session on a real issue, then save one automation. That is a lunch break’s worth of effort, and it is enough to know whether the model fits how you work. 

Have you given the GitHub Copilot app a proper go, or are you still loyal to your IDE? Tell me what won you over, or what sent you back, in the comments. 

Read the whole story
alvinashcraft
1 minute ago
reply
Pennsylvania, USA
Share this story
Delete

Reading Notes #710

1 Share

This week’s collection highlights several key advancements in Azure performance, the nuances of orchestrating multiple AI agents, and critical updates to NuGet security. These pieces offer practical insights for anyone looking to streamline their development workflow while maintaining a more secure infrastructure.


Cloud

AI

DevOps

~frank
Read the whole story
alvinashcraft
1 minute ago
reply
Pennsylvania, USA
Share this story
Delete
Next Page of Stories