Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Application Architecture for Scaled Agile

Application Architecture for Scaled Agile

An agile software development was introduced to focus more on individuals, interactions, working software, customer collaboration and responding to change. An agile team usually consists of 4 to 6 members including product owner, testers, designers, developers and agile coach. It is straightforward for smaller companies to apply. As a company grows, you might wonder how to keep the agile. Spotify has mainly formed agile teams based on features. However, teams’ autonomous could be restricted by an application architecture. We’ve changed an application architecture to support a scaled agile. Several ideas are borrowed from a longest time used architecture, WWW(world wide web). In this talk, we describe our application architecture with lessons.

Sangsoo Nam

April 10, 2016
Tweet

More Decks by Sangsoo Nam

Other Decks in Technology

Transcript

  1. Sangsoo Nam Android Engineer at Spotify sangsoonam.github.io @sangsoonam • Full

    stack Engineer • Scrum Master • Software Tester
  2. Agile Manifesto Individuals and interactions over processes and tools Working

    software over comprehensive documentation http:/ /agilemanifesto.org/
  3. Agile Manifesto Individuals and interactions over processes and tools Working

    software over comprehensive documentation Customer collaboration over contract negotiation http:/ /agilemanifesto.org/
  4. Agile Manifesto Individuals and interactions over processes and tools Working

    software over comprehensive documentation Customer collaboration over contract negotiation Responding to change over following a plan http:/ /agilemanifesto.org/
  5. Agile Development Cycle Sprint Backlog 2-3 weeks Daily Meeting 24

    hours Shippable Product Increment Product Backlog
  6. ARTIST ALBUM CHART RUNNING RADIO CONCERT DISCOVER SEARCH BROWSE …

    ... … … … … … … … … … … … … … … … … … … … … … … … …
  7. “Any organization that designs a system will produce a design

    whose structure is a copy of the organization's communication structure.” Conway’s law by Melvin Conway
  8. Click Open ChartActivity gridView.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?>

    parent, View view, int position, long id) { Intent i = new Intent(getContext(), ChartActivity.class); i.putExtra(EXTRA_CHART_ID, chartIds.get(position)); startActivity(i); } });
  9. Click Open ChartActivity gridView.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?>

    parent, View view, int position, long id) { Intent i = new Intent(getContext(), ChartActivity.class); i.putExtra(EXTRA_CHART_ID, chartIds.get(position)); startActivity(i); } });
  10. Christmas 50 This is not the Chart but the Playlist!

    Show Christmas 50 for Christmas season
  11. Open PlaylistActivity gridView.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent,

    View view, int position, long id) { if (position == 3) { Intent i = new Intent(getContext(), PlatlistActivity.class); i.putExtra(EXTRA_PLATLIST_ID, chartIds.get(position)); startActivity(i); } else { Intent i = new Intent(getContext(), ChartActivity.class); i.putExtra(EXTRA_CHART_ID, chartIds.get(position)); startActivity(i); } } });
  12. Open PlaylistActivity gridView.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent,

    View view, int position, long id) { if (position == 3) { Intent i = new Intent(getContext(), PlatlistActivity.class); i.putExtra(EXTRA_PLATLIST_ID, chartIds.get(position)); startActivity(i); } else { Intent i = new Intent(getContext(), ChartActivity.class); i.putExtra(EXTRA_CHART_ID, chartIds.get(position)); startActivity(i); } } });
  13. Open AlbumActivity gridView.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent,

    View view, int position, long id) { Intent i = new Intent(getContext(), AlbumActivity.class); i.putExtra(EXTRA_ALBUM_ID, albumIds.get(position)); startActivity(i); } }); Click
  14. Open AlbumActivity gridView.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent,

    View view, int position, long id) { Intent i = new Intent(getContext(), AlbumActivity.class); i.putExtra(EXTRA_ALBUM_ID, albumIds.get(position)); startActivity(i); } });
  15. Consider Feature Flag gridView.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?>

    parent, View view, int position, long id) { Intent i; if(FeatureFlags.get("NewAlbum")) { i = new Intent(getContext(), NewAlbumActivity.class); } else { i = new Intent(getContext(), AlbumActivity.class); } i.putExtra(EXTRA_ALBUM_ID, albumIds.get(position)); startActivity(i); } });
  16. Consider Feature Flag gridView.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?>

    parent, View view, int position, long id) { Intent i; if(FeatureFlags.get("NewAlbum")) { i = new Intent(getContext(), NewAlbumActivity.class); } else { i = new Intent(getContext(), AlbumActivity.class); } i.putExtra(EXTRA_ALBUM_ID, albumIds.get(position)); startActivity(i); } });
  17. Consider Feature Flag gridView.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?>

    parent, View view, int position, long id) { Intent i; if(FeatureFlags.get("NewAlbum")) { i = new Intent(getContext(), NewAlbumActivity.class); } else { i = new Intent(getContext(), AlbumActivity.class); } i.putExtra(EXTRA_ALBUM_ID, albumIds.get(position)); startActivity(i); } });
  18. No Feature Flag Logic gridView.setOnItemClickListener(new OnItemClickListener() { @Override public void

    onItemClick(AdapterView<?> parent, View view, int position, long id) { Intent i = SpotifyUriResolver.resolve(
 getContext(), uris.get(position) ); startActivity(i); } });
  19. No Feature Flag Logic gridView.setOnItemClickListener(new OnItemClickListener() { @Override public void

    onItemClick(AdapterView<?> parent, View view, int position, long id) { Intent i = SpotifyUriResolver.resolve(
 getContext(), uris.get(position) ); startActivity(i); } });
  20. No Feature Flag Logic gridView.setOnItemClickListener(new OnItemClickListener() { @Override public void

    onItemClick(AdapterView<?> parent, View view, int position, long id) { Intent i = SpotifyUriResolver.resolve(
 getContext(), uris.get(position) ); startActivity(i); } }); Just resolve the URI
  21. Open PlaylistActivity gridView.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent,

    View view, int position, long id) { if (position == 3) { Intent i = new Intent(getContext(), PlatlistActivity.class); i.putExtra(EXTRA_PLATLIST_ID, chartIds.get(position)); startActivity(i); } else { Intent i = new Intent(getContext(), ChartActivity.class); i.putExtra(EXTRA_CHART_ID, chartIds.get(position)); startActivity(i); } } });
  22. No PlaylistActivity Logic gridView.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?>

    parent, View view, int position, long id) { Intent i = SpotifyUriResolver.resolve(
 getContext(), uris.get(position) ); startActivity(i); } });
  23. No PlaylistActivity Logic gridView.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?>

    parent, View view, int position, long id) { Intent i = SpotifyUriResolver.resolve(
 getContext(), uris.get(position) ); startActivity(i); } });
  24. No PlaylistActivity Logic gridView.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?>

    parent, View view, int position, long id) { Intent i = SpotifyUriResolver.resolve(
 getContext(), uris.get(position) ); startActivity(i); } });
  25. No PlaylistActivity Logic gridView.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?>

    parent, View view, int position, long id) { Intent i = SpotifyUriResolver.resolve(
 getContext(), uris.get(position) ); startActivity(i); } }); Just resolve the URI
  26. Consistency … ... … … … … … … …

    … … … … … … … … … … … … … … … … …
  27. Don’t (Inline Style) <button style="width:100px;height:30px;border:1px solid;padding:10px" type="button">Save</button> Do (Global Style)

    .button { width: 100px; height: 30px; border: 1px solid; padding: 10px } <button type="button">Save</button> CSS(Cascading Style Sheets)
  28. HTML (Hyper Text Markup Language) Server Request Web Browser Response

    (HTML) <div class="offer"> <h3 class="offer-title">Spotify Family</h3> <a class="btn btn-link btn-sm" href="/us/family/">Learn more</a> ...
  29. Response { "text":"Today's Top Hits", "image": { ... }, "link":

    { ... "uri": "spotify:user:spotify:playlist:5FJXhjdILmRA2z5bvz4nzf" }, }, { "text":"RapCaviar", "image": { ... }, "link": { ... "uri": "spotify:user:spotify:playlist:5yolys8XG4q7YfjYGl5Lff" }, }
  30. Response with Render Type { "card": { "text":"Today's Top Hits",

    "image": { ... }, "link": { ... "uri": "spotify:user:spotify:playlist:5FJXhjdILmRA2z5bvz4nzf" }, } }, { "card": { "text":"RapCaviar", "image": { ... }, "link": { ... "uri": "spotify:user:spotify:playlist:5yolys8XG4q7YfjYGl5Lff" }, } }
  31. Response with Render Type { "card": { "text":"Today's Top Hits",

    "image": { ... }, "link": { ... "uri": "spotify:user:spotify:playlist:5FJXhjdILmRA2z5bvz4nzf" }, } }, { "card": { "text":"RapCaviar", "image": { ... }, "link": { ... "uri": "spotify:user:spotify:playlist:5yolys8XG4q7YfjYGl5Lff" }, } }
  32. Response with Render Type { "carousel": { "items": [ {

    "card": { "text":"Today's Top Hits", "image": { ... }, "link": { ... "uri": "spotify:user:spotify:playlist:5FJXhjdILmRA2z5bvz4n }, } }, { "card": { "text":"RapCaviar", "image": { ... }, "link": { ... ]
  33. Response with Render Type { "carousel": { "items": [ {

    "card": { "text":"Today's Top Hits", "image": { ... }, "link": { ... "uri": "spotify:user:spotify:playlist:5FJXhjdILmRA2z5bvz4n }, } }, { "card": { "text":"RapCaviar", "image": { ... }, "link": { ... ]
  34. Response with Render Type { "carousel": { "items": [ {

    "card": { "text":"Today's Top Hits", "image": { ... }, "link": { ... "uri": "spotify:user:spotify:playlist:5FJXhjdILmRA2z5bvz4n }, } }, { "card": { "text":"RapCaviar", "image": { ... }, "link": { ... ]
  35. Response with Header { "header": { "text": "Popular Playlists" }

    }, { "card": { "text":"Today's Top Hits", "image": { ... }, "link": { ... "uri": "spotify:user:spotify:playlist:5FJXhjdILmRA2z5bvz4nzf" }, } } ...
  36. Response with Header { "header": { "text": "Popular Playlists" }

    }, { "card": { "text":"Today's Top Hits", "image": { ... }, "link": { ... "uri": "spotify:user:spotify:playlist:5FJXhjdILmRA2z5bvz4nzf" }, } } ...
  37. Response with Header { "header": { "text": "Popular Playlists" }

    }, { "card": { "text":"Today's Top Hits", "image": { ... }, "link": { ... "uri": "spotify:user:spotify:playlist:5FJXhjdILmRA2z5bvz4nzf" }, } } ...
  38. ARTIST ALBUM CHART RUNNING RADIO CONCERT DISCOVER SEARCH BROWSE …

    ... … … … … … … … … … … … … … … … … … … … … … … … …
  39. • Dependency • Consistency Architecture for Scaled Agile RUNNING CHART

    BROWSE GENRE Spotify URI Resolver … GLUE From URL
  40. • Dependency • Consistency Architecture for Scaled Agile RUNNING CHART

    BROWSE GENRE Spotify URI Resolver … GLUE From CSS From URL
  41. • Dependency • Consistency • Dynamic Architecture for Scaled Agile

    RUNNING CHART BROWSE GENRE Spotify URI Resolver … GLUE From CSS From URL
  42. • Dependency • Consistency • Dynamic Architecture for Scaled Agile

    RUNNING CHART BROWSE GENRE Spotify URI Resolver … GLUE Native Renderer From CSS From URL
  43. • Dependency • Consistency • Dynamic Architecture for Scaled Agile

    RUNNING CHART BROWSE GENRE Spotify URI Resolver … GLUE Native Renderer From CSS From URL From HTML